Grpc Testing on MacOS with protobuf-net.Grpc.AspNetCore - c#

I'm having a hard time trying to figure out how to appropriately test Grpc with Authentication/Authorization on MacOS.
System.ArgumentException: CallCredentials can't be composed with InsecureCredentials. CallCredentials must be used with secure channel credentials like SslCredentials.
I'm unable to test locally using SSL as it appears its not supported with HTTP2 on MacOS.
options.ListenAnyIP(8080);
options.ListenAnyIP(8585, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
And here is how the channel is created
private async Task<GrpcChannel> CreateAuthenticatedChannel()
{
var token = await ApiClient.GetTokenAsync();
var credentials = CallCredentials.FromInterceptor((context, metadata) =>
{
metadata.Add("Authorization", $"Bearer {token}");
return Task.CompletedTask;
});
var channel = GrpcChannel.ForAddress("http://localhost:8585", new GrpcChannelOptions
{
Credentials = ChannelCredentials.Create(ChannelCredentials.Insecure, credentials),
UnsafeUseInsecureChannelCallCredentials = true
});
return channel;
}
If I attempt to switch the HTTP2 port to SSL on MacOS, I get the following error
HTTP/2 over TLS is not supported on macOS due to missing ALPN support.

Related

Kestrel ASP.Net Core 6.0 handling of expired client certificates - different behaviour when CustomTrustStore is used

I am using a self-hosted Kestrel Asp Net Core web server and am testing the behaviour when a client sends an expired certificate. The test is peformed with two scenarios, one with the root certificate imported into the Windows certificate store, and one using an application internal custom certificate store. The server uses a pfx file to construct a X509Certificate2 object.
Here is the code sample:
private void InitServices(WebApplicationBuilder builder, bool useCustomCertificateStore)
{
var webBuilder = builder.WebHost;
// load certificate from pfx file
var rootCert = new X509Certificate2(rootCertPfxFilePath, rootCertPfxFilePassword);
webBuilder.ConfigureKestrel(o =>
{
if (useCustomCertificateStore)
{
o.ConfigureHttpsDefaults(o =>
{
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
o.AllowAnyClientCertificate();
o.CheckCertificateRevocation = false;
});
}
else
{
o.ConfigureHttpsDefaults(o =>
{
o.ServerCertificate = rootCert;
o.ClientCertificateMode = ClientCertificateMode.RequireCertificate;
});
}
});
webBuilder.UseKestrel(o =>
{
o.Listen(IPAddress.Parse(hostSettings.HttpsEndPoint.IpAddr), hostSettings.HttpsEndPoint.Port,
listenOptions =>
{
HttpsConnectionAdapterOptions httpsConnectionAdapterOptions = null;
if (useCustomCertificateStore)
{
httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
ServerCertificate = rootCert
};
httpsConnectionAdapterOptions.AllowAnyClientCertificate();
}
else
{
httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
{
ClientCertificateMode = ClientCertificateMode.RequireCertificate,
SslProtocols = System.Security.Authentication.SslProtocols.None,
ServerCertificate = rootCert,
ClientCertificateValidation = (x509Cert, x509Chain, sslErrors) =>
{
logger.LogInformation($"Certificate validation request");
if (sslErrors == SslPolicyErrors.None)
{
return true;
}
else
{
logger.LogError($"Certificate validation failed because of SslPolicyErrors <{sslErrors.ToString()}>");
}
return false;
}
};
}
listenOptions.UseHttps(httpsConnectionAdapterOptions);
});
});
var services = builder.Services;
services
.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
// custom certificate store, if windows certificate store should not be used!
if (useCustomCertificateStore)
{
options.ChainTrustValidationMode = X509ChainTrustMode.CustomRootTrust;
options.CustomTrustStore = new X509Certificate2Collection { rootCert };
options.RevocationMode = X509RevocationMode.NoCheck;
}
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
return Task.CompletedTask;
},
OnAuthenticationFailed = context =>
{
logger?.LogError($"OnAuthenticationFailed with <{context.Exception.Message}> (host <{context.Request.Host}>, method <{context.Request.Method}>, path <{context.Request.Path}>)");
context.Fail("invalid cert");
return Task.CompletedTask;
}
};
});
// add authorization to require auth by default for all routes
services.AddAuthorization(options =>
{
options.FallbackPolicy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
});
.....
}
I am using curl to call different endpoints on the web server. The tests with valid certificates are executed successfully. The tests with an expired certificate show a different behaviour of the kestrel web server, dependently if the custom certificate store is used or not (in this case the windows certificate store contains the certificate).
If the PC where the kestrel web server is running has the root certificate imported into the Windows Certificate Store, the kestrel web server sends an error message pertaining to the TLS handshake -> TLS alert certificate expired, and no http response is sent at all.
If the kestrel web server uses a custom certificate store, the kestrel web server sends a 403 http response.
Why is the behaviour different ? Can the kestrel web server be configured to always send a 403 http response in such scenarios?

SignalR connection falls back to longpolling because of 401

I have a .net 6 backend with an Angular 13 backend, that uses JWT tokens for auth. For some reason SignalR always falls back to longpolling, both on prod and on dev machine, it seems that it's call to negotiate?negotiateVersion=1 is successful, and it chooses WebSockets, but afterwards it's call to localhost:PORT/hubs/myhub?id=[ID]&access_token=[JWTTOKEN] is returned with a 401.
The Angular part is using NGRX to get the JWT token, and the JWT token expires after 5 minutes. When it receives a 401 after connection is established it disconnects, makes a normal renew call, and connects again with the new JWT token. However the request described above will always return 401 even with a valid token.
My SignalR service:
export class NotificationSignalrService {
private connection: signalR.HubConnection;
connectionClosedRefreshTokenSubscription: Subscription | undefined;
startConnectionRefreshTokenSubscription: Subscription | undefined;
constructor(#Inject(APP_CONFIG) private appConfig: any, private store: Store) {
this.connection = new signalR.HubConnectionBuilder()
.withUrl(`${this.appConfig.SIGNALR}/hubs/notificationhub`, this.hubConnectionOptions)
.configureLogging(signalR.LogLevel.Debug)
//.withAutomaticReconnect()
.build();
this.connection.onclose(error => {
console.log(`Forbindelse lukket pga: ${error}`);
this.store.dispatch(AuthActions.renewNoLoading());
this.connectionClosedRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
next: tokenRefreshed => {
if (tokenRefreshed) {
this.connectionClosedRefreshTokenSubscription?.unsubscribe();
this.startSignalRConnection();
}
}
})
});
this.startSignalRConnection();
this.startListening();
}
startSignalRConnection() {
this.connection.start().catch(error => {
console.log(`Der skete en fejl ved start af signalR ${error}`);
this.startConnectionRefreshTokenSubscription = this.store.select(AuthSelectors.selectTokenRefreshed).subscribe({
next: tokenRefreshed => {
if (tokenRefreshed) {
this.startConnectionRefreshTokenSubscription?.unsubscribe();
this.connection.start().catch(error => console.log(`Kunne ikke starte forbindelsen efter renew ${error}`));
}
}
})
});
}
#HostListener('window:beforeunload', ['$event'])
beforeunloadHandler() {
this.connection.stop();
}
protected get hubConnectionOptions(): IHttpConnectionOptions {
// NOTE: The auth token must be updated for each request. So using headers option is not true.
// Also for websockets and some other protocols signalr cannot set auth headers.
// See https://docs.microsoft.com/en-us/aspnet/core/signalr/authn-and-authz?view=aspnetcore-5.0#bearer-token-authentication
return {
/*headers,*/
accessTokenFactory: () => {
return this.store.select(AuthSelectors.getLoggedInToken)
.pipe(take(1), filter(x => x !== null), map(x => x === null ? "" : x)).toPromise();
// this.authService.refreshLogin()
// .pipe(map(_ => this.authService.accessToken)).toPromise();
}
};
// NOTE:
// The access token function you provide is called before every HTTP request made by SignalR. If you need to renew the token in order to keep the connection active (because it may expire during the connection), do so from within this function and return the updated token.
// In standard web APIs, bearer tokens are sent in an HTTP header. However, SignalR is unable to set these headers in browsers when using some transports. When using WebSockets and Server - Sent Events, the token is transmitted as a query string parameter.
}
getAuthToken() {
let token = '';
this.store.select(AuthSelectors.getLoggedInToken).pipe(take(1))
.subscribe(authToken => token = authToken ?? "");
return {
Authorization: `Bearer ${token}`
};
}
startListening() {
this.connection.on("NewNotificationForUser", (notification: NotificationsEntity) =>
this.store.dispatch(NotificationsState.NotificationsActions.newNotification({ notification }))
);
}
in .net under Startup i have services.AddSignalR(); under ConfigureServices and
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
endpoints.MapHub<NotificationHub>("/hubs/notificationhub");
});
in Configure
My Hub has a [Authorize] attribute.
You probably aren't handling the access_token query string parameter. This is required when using WebSockets because the browser APIs for WebSockets do not support setting headers.
The docs explain how to handle the query string
https://learn.microsoft.com/aspnet/core/signalr/authn-and-authz?view=aspnetcore-7.0#built-in-jwt-authentication

Getting client certificate

I need to implement Client Certificate authentication on some of the endpoints in my .NET 5 Web API. So I don't want to enable HTTPS across all endpoint as described here in the MS docs. I am using Kestrel on my local machine and not IIS express or IIS.
I have tried the following three methods with no luck on either of them:
var clientCertHeaders = context.HttpContext.Request.Headers;
This one returns the normal headers for the request but no certificate.
var clientCert = context.HttpContext.Connection.ClientCertificate;
var clientCertAsync = context.HttpContext.Connection.GetClientCertificateAsync().Result;
These two both return null.
I've tried applying the following to my services:
services.AddCertificateForwarding(options =>
{
options.CertificateHeader = "X-SSL-CERT";
options.HeaderConverter = (headerValue) =>
{
X509Certificate2 clientCertificate = null;
if(!string.IsNullOrWhiteSpace(headerValue))
{
var bytes = Encoding.UTF8.GetBytes(headerValue);
clientCertificate = new X509Certificate2(bytes);
}
return clientCertificate;
};
});
Even with that enabled in my services I am not retrieving the client certificate.
I am using Postman to make the requests to the API requests.
You need to configure Kestrel to allow client certificates in the program.cs The default value is ClientCertificateMode.NoCertificate so in your ConfigureWebHostDefaults you need to change that to ClientCertificateMode.AllowCertificate.
Here's an edited chunk of code from the docs you sent where I did that:
public static IHostBuilder CreateHostBuilder(string[] args)
{
return Host.CreateDefaultBuilder(args)
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
webBuilder.ConfigureKestrel(o =>
{
o.ConfigureHttpsDefaults(o =>
o.ClientCertificateMode =
ClientCertificateMode.AllowCertificate);
});
});
}

AspNetCore rejects preflight messages

We have a HttpSys listener which should accept authentication as either NTLM, Negotiate or JWT.
Problem is that it looks like HttpSys rejects both preflight messages and messages with Bearer token (JWT)
Our listener is build like this
_host = new WebHostBuilder()
.UseHttpSys(options =>
{
options.Authentication.Schemes = AuthenticationSchemes.NTLM | AuthenticationSchemes.Negotiate;
options.Authentication.AllowAnonymous = false;
})
.UseUrls($"http://+:{PortNo}/")
.UseUnityServiceProvider(IocContainer)
.ConfigureServices(services => { services.AddSingleton(_startUpConfig); })
.UseStartup<StartUp>()
.Build();
We add CORS and Authentication to services:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(o => o.AddPolicy("AllowAll", builder =>
{
builder.AllowAnyOrigin().AllowAnyMethod().AllowAnyHeader().AllowCredentials().WithOrigins("*");
}));
services.AddAuthentication(o =>
{
o.DefaultAuthenticateScheme = HttpSysDefaults.AuthenticationScheme;
o.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(o =>
{
o.Events = new JwtBearerEvents { OnTokenValidated = context => AuthMiddleWare.VerifyJwt(context, _jwtPublicKey) };
});
We run an angular application in Chrome, which is rejected with the following error message
"Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. "
Also any Bearer token message is rejected. Debugging reveals that our code to verify JWT bearer is never reached (AuthMiddleWare.VerifyJwt)
My guess is that HttpSys rejects any message not carrying Either Ntlm or Negotiate token. Only I have no idea how to fix that
In .net Framework we used the AuthenticationSchemeSelectorDelegate to run the following code, which allowed OPTIONS messages and messages with Bearer token to pass through the HttpSys listener
public AuthenticationSchemes EvaluateAuthentication(HttpListenerRequest request)
{
if (request.HttpMethod == "OPTIONS")
{
return AuthenticationSchemes.Anonymous;
}
if (request.Headers["Authorization"] != null && request.Headers["Authorization"].Contains("Bearer "))
{
return AuthenticationSchemes.Anonymous;
}
return AuthenticationSchemes.IntegratedWindowsAuthentication;
}
We have this working now. Basically the problem was that allowing all three authentication methods is not a supported scenario in Asp Net Core.
So the trick was to implement our own authentication in the pipeline.
Also see this github issue:
https://github.com/aspnet/AspNetCore/issues/13135

IdentityServer3 - rejected because invalid CORS path

We have an ASP.NET MVC application that is authenticating without issue against IdentityServer3, however the web API part of the application using ApiController's start to fail if the user waits before proceeding with AJAX functionality after about 3 minutes (before 3 mins everything seems fine).
The errors seen in Chrome are:
XMLHttpRequest cannot load
https://test-auth.myauthapp.com/auth/connect/authorize?client_id=ecan-farmda…gwLTk5ZjMtN2QxZjUyMjgxNGE4MDg2NjFhZTAtOTEzNi00MDE3LTkzNGQtNTc5ODAzZTE1Mzgw.
No 'Access-Control-Allow-Origin' header is present on the requested
resource. Origin 'http://test.myapp.com' is therefore not allowed
access.
On IE I get the following errors:
SCRIPT7002: XMLHttpRequest: Network Error 0x4c7, The operation was
canceled by the user.
Looking at IdentityServer3's logs I'm seeing entries like so:
2015-08-10 16:42 [Warning]
(Thinktecture.IdentityServer.Core.Configuration.Hosting.CorsPolicyProvider)
CORS request made for path: /connect/authorize from origin:
http://test.myapp.com but rejected because invalid CORS path
In the IdentityServer3 web application I'm giving clients AllowedCorsOrigins:
Thinktecture.IdentityServer.Core.Models.Client client = new Thinktecture.IdentityServer.Core.Models.Client()
{
Enabled = configClient.Enabled,
ClientId = configClient.Id,
ClientName = configClient.Name,
RedirectUris = new List<string>(),
PostLogoutRedirectUris = new List<string>(),
AllowedCorsOrigins = new List<string>(),
RequireConsent = false, // Don't show consents screen to user
RefreshTokenExpiration = Thinktecture.IdentityServer.Core.Models.TokenExpiration.Sliding
};
foreach (Configuration.RegisteredUri uri in configClient.RedirectUris)
{
client.RedirectUris.Add(uri.Uri);
}
foreach (Configuration.RegisteredUri uri in configClient.PostLogoutRedirectUris)
{
client.PostLogoutRedirectUris.Add(uri.Uri);
}
// Quick hack to try and get CORS working
client.AllowedCorsOrigins.Add("http://test.myapp.com");
client.AllowedCorsOrigins.Add("http://test.myapp.com/"); // Don't think trailing / needed, but added just in case
clients.Add(client);
And when registering the service I add a InMemoryCorsPolicyService:
app.Map("/auth", idsrvApp =>
{
var factory = new IdentityServerServiceFactory();
factory.Register(new Registration<AuthContext>(resolver => AuthObjects.AuthContext));
factory.Register(new Registration<AuthUserStore>());
factory.Register(new Registration<AuthRoleStore>());
factory.Register(new Registration<AuthUserManager>());
factory.Register(new Registration<AuthRoleManager>());
// Custom user service used to inject custom registration workflow
factory.UserService = new Registration<IUserService>(resolver => AuthObjects.AuthUserService);
var scopeStore = new InMemoryScopeStore(Scopes.Get());
factory.ScopeStore = new Registration<IScopeStore>(scopeStore);
var clientStore = new InMemoryClientStore(Clients.Get());
factory.ClientStore = new Registration<IClientStore>(clientStore);
var cors = new InMemoryCorsPolicyService(Clients.Get());
factory.CorsPolicyService = new Registration<ICorsPolicyService>(cors);
...
var options = new IdentityServerOptions
{
SiteName = "Authentication",
SigningCertificate = LoadCertificate(),
Factory = factory,
AuthenticationOptions = authOptions
};
...
});
I do note that the IdentityServer3 log entries say "CORS request made for path: /connect/authorize" rather than "CORS request made for path: /auth/connect/authorize". But looking through the IdentityServer3 source code suggests this probably isn't the issue.
Perhaps the InMemoryCorsPolicyService isn't being picked up?
Any ideas of why things aren't working for the AJAX called ApiController?
Thinktecture.IdevtityServer3 v1.6.2 has been installed using NuGet.
Update
I'm having a conversation with the IdentityServer3 developer, but am still having an issue reaching a resolution. In case it helps:
https://github.com/IdentityServer/IdentityServer3/issues/1697
Did you try adding https url also?- client.AllowedCorsOrigins.Add("https://test.myapp.com");
The documentation of IdentityServer says you should configure it on the client:
AllowedCorsOrigins = ... // Defaults to the discovery, user info, token, and revocation endpoints.
https://docs.duendesoftware.com/identityserver/v6/reference/options/#cors
CORS is a nightmare!
It's a browser thing which is why you're witnessing different behaviour in IE than in Chrome.
There are (at least) two ways that CORS is configured on the server. When a client makes a request with the Origin header you have to tell the server whether or not to accept it -- if accepted then the server adds the Access-Control-Allow-Origin header to the response for the browser.
In MVC / webAPI you have to add CORS services, set a CORS policy, and then .UseCors something like this:
builder.Services.AddCors((options =>
{
if (settings.AllowedCorsOrigins.Length > 0)
{
options.AddDefaultPolicy(builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().WithOrigins(settings.AllowedCorsOrigins);
});
}
if (isDevelopment)
{
options.AddPolicy("localhost", builder =>
{
builder.SetIsOriginAllowedToAllowWildcardSubdomains();
builder.AllowAnyHeader().AllowAnyMethod().SetIsOriginAllowed((string origin) => { return origin.Contains("localhost"); }); });
}
});
and
app.UseCors();
if (app.Environment.IsDevelopment())
{
app.UseCors("localhost");
}
Typically, you want the list of allowed hosts as an array of strings in your appsettings.json. And watch out for the boobytrap with SetIsOriginAllowedToAllowWildcardSubdomains.
As well as this, IdentityServer has its own additional CORS settings which are applied in addition to the standard MVC/webAPI settings. These are in the ClientCorsOrigin table and this doesn't support wildcard subdomains. You can sidestep this whole boobytrap by implementing your own ICorsPolicyService to use the same settings from your appsettings.json something like this
public class CorsPolicyService : ICorsPolicyService
{
private readonly CorsOptions _options;
public CorsPolicyService(IOptions<CorsOptions> options)
{
_options = options.Value;
}
private bool CheckHost(string host)
{
foreach (string p in _options.AllowedCorsOrigins)
{
if (Regex.IsMatch(host, Regex.Escape(p).Replace("\\*", "[a-zA-Z0-9]+"))) // Hyphen?
{
return true;
}
}
return false;
}
public Task<bool> IsOriginAllowedAsync(string origin)
{
return Task.FromResult(CheckHost(origin));
}
}

Categories