I make log with CorrelationId in Serilog for email notification and tracking pixel in ASP.NET Core 6 Web API - asp.net-core-webapi

I make log with CorrelationId in Serilog for email service sender and tracking pixel in asp.net core 6 web api , each message log with GUID correlationId and when user see the message we can also log the time he opens the email message but problem is I want to make same correlationId log for both email message and the time user see the message but each action in this app make their own correlationID.
I think the problem is in middleware correlation and program.cs. Does anyone know how I could do that? Or is it even possible?
Thanks in advance.
The class for send email and send pixel tracking
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.Extensions.Options;
using MimeKit;
using MimeKit.Text;
using DAL.Model;
using ImageTracker.Service;
namespace SimpleEmailApp.Services
{
public class MailService : IMailService
{
private readonly AppSetting _mailSetting;
private readonly ILogger<MailService> _logger;
private readonly IWhichMessageIsRead _whichMessageIsRead;
// IWhichMessageIsRead whichMessageIsRead
public MailService(IOptions<AppSetting> mailSetting, ILogger<MailService> logger,
IWhichMessageIsRead whichMessageIsRead)
{
_mailSetting = mailSetting.Value;
_logger = logger;
_whichMessageIsRead = whichMessageIsRead;
}
public async Task SendEmailAsync(EmailMessage message)
{
var email = new MimeMessage();
try
{
email.Sender = MailboxAddress.Parse(_mailSetting.Mail);
email.To.Add(MailboxAddress.Parse(messsage.Reciver));
email.Subject = messsage.Subject;
// a unique-id to attach with the mail
// var messageId = Guid.NewGuid();
// bool IsRead = false;
// the server that receives the request from the image and process it
// var deliveryProcessor = "https://localhost:7156/api/Track";
// constructing the image tag with "src" pointing to the external resource
// passing the unique-id in the query-string.
var imgTag = string.Format(#"<img src=""https://localhost:7156/api/Mail"" alt ="""" width = ""1"" height = ""1""
style=""width: 0px; height: 0px; border:0px;""/>");
var builder = new BodyBuilder();
builder.HtmlBody = imgTag;
email.Body = new TextPart(TextFormat.Html)
{
Text = message.Body + " " + imgTag
};
var lg = _logger.LogInformation("");
_logger.LogInformation("Email {#mailRequest} creates for {Sender} at {now}.", message, _mailSetting.Mail, DateTime.Now);
_whichMessageIsRead. GetMessage( ,messsage);
}
catch (Exception)
{
_logger.LogError("Email is not sent.");
throw;
}
using var smtp = new SmtpClient();
try
{
smtp.Connect(_mailSetting.Host, _mailSetting.Port, SecureSocketOptions.StartTls);
smtp.Authenticate(_mailSetting.Mail, _mailSetting.Password);
await smtp.SendAsync(email);
smtp.Disconnect(true);
}
catch (Exception)
{
_logger.LogError(" SMTP Server Configuration Error occurred ");
throw;
}
}
}
}
My middleware for implementing correlationid:
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
namespace CorrelationProvider.Middleware
{
public class LogHeaderMiddleware
{
private readonly RequestDelegate _next;
public LogHeaderMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task InvokeAsync(HttpContext context)
{
var header = context.Request.Headers["X-SessionId"];
if (header.Count > 0)
{
var logger = context.RequestServices.GetRequiredService<ILogger<LogHeaderMiddleware>>();
using (logger.BeginScope("{#SessionId}", header[0]))
{
await _next(context);
}
}
else
{
await _next(context);
}
}
}
}
program.cs:
global using SimpleEmailApp.Services;
using DAL.Model;
using SimpleEmailApp.ConfgureSetting;
using Serilog;
using CorrelationProvider.Middleware;
using CorrelationProvider;
using Microsoft.AspNetCore.Mvc;
//using ImageTracker.Middleware;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddControllers();
// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddControllers();
//builder.Host.UseSerilog();
var Logger = new LoggerConfiguration()
.ReadFrom.Configuration(builder.Configuration)
.Enrich.FromLogContext()
.Enrich.WithCorrelationId()
.CreateLogger();
builder.Logging.ClearProviders();
builder.Logging.AddSerilog(Logger);
builder.Services.AddSwaggerGen();
/// <summary>
/// correlation provider
/// </summary>
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<ISessionIdAccessor, SessionIdAccessor>();
/// <summary>
/// /below confiure method is add appsettings.development.json values to appsettings.json and
/// configure host(smtp server settings) for us .
/// also congigure method is made by mailsetting c# class properties and fill with mailsetting in
/// appsettings.development.json file
/// we can use appsettings.json instead appsettings.development.json .
/// </summary>
//builder.Services.Configure<AppSetting>(builder.Configuration.GetSection("MailSetting"));
builder.Services.Configure<AppSetting>(builder.Configuration.GetSection("MailSetting"));
builder.Services.ConfigureWritable<AppSetting>(builder.Configuration.GetSection("MailSetting"));
//builder.Services.AddSingleton<FileContentResult>(new FileContentResult(
// Convert.FromBase64String(builder.Configuration.GetValue<String>("Response:PixelContentBase64")),
// builder.Configuration.GetValue<String>("Response:PixelContentType")
// ));
builder.Host.ConfigureAppConfiguration((hostingContext, config) =>
{
config.AddJsonFile("MailSetting",
optional: true,
reloadOnChange: true);
});
builder.Services.AddScoped<IMailService, MailService>();
/// <summary>
/// correlation Id
/// </summary>
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthorization();
app.UseMiddleware<LogHeaderMiddleware>();
//app.UseMiddleware<EmailTrackMiddleware>();
/// <summary>
/// static files for sending 1x1 pic
/// </summary>
//app.UseDefaultFiles();
//app.UseStaticFiles();
app.UseDeveloperExceptionPage();
app.MapControllers();
app.Run();
My appsettings.json file:
{
"MailSetting": {
"Mail": "michelminestein#gmail.com",
"Password": "quafcvofqjwfvhqu",
"Host": "smtp.gmail.com",
"Port": 587
},
"Serilog": {
"Using": [
"Serilog.Sinks.File",
"Serilog.Sinks.Console"
],
"MinimumLevel": {
"Default": "Information",
"Override": {
"SimpleEmailApp.Controllers": "Information",
"Microsoft": "Information",
"Microsoft.AspNetCore": "Warning",
"System": "Warning"
}
},
"WriteTo": [
{
"Name": "Console",
"Args": {
"outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff zzz} {Level:u11}] {CorrelationId} {Message:lj}{NewLine}{Exception}",
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console",
"formatter": "Serilog.Formatting.Compact.RenderedCompactJsonFormatter, Serilog.Formatting.Compact"
}
},
{
"Name": "File",
"Args": {
"path": "../logs/EmailApi-.log",
"rollingInterval": "Day",
"outputTemplate": "[{Timestamp:yyyy/MM/dd HH:mm:ss.fff zzz} {Level:u11}] {CorrelationId} {Message:lj}{NewLine}{Exception}"
}
}
]
},
"AllowedHosts": "*"
}
Any solution to solve this error?

One possible I forgot to mention is in message model I can add message ID and GUID in it, with this solution we can track which message is read by client

Related

Blazor not receiving signalr created on server side

Following many tutorials, examples, this example below I call on the server side, but the client side does not receive, sometimes it works but sometimes it doesn’t (more doesn’t work than it works)
It was supposed to be very simple, but it's not, any suggest will help me so much!
Server side
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().AddNewtonsoftJson(options =>
{
options.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
});
var connection = #"data source=comandai.database.windows.net;initial catalog=HojeTaPago;persist security info=True;user id=Comandai;password=Ck#21112009;MultipleActiveResultSets=True;";
services.AddDbContext<ComandaiContext>(options => options.UseSqlServer(connection));
services.AddSignalR(options => options.KeepAliveInterval = TimeSpan.FromSeconds(5));
services.Configure<CookiePolicyOptions>(options =>
{
options.CheckConsentNeeded = context => true;
options.MinimumSameSitePolicy = SameSiteMode.None;
});
services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo { Title = "HojeTaPago API", Version = "v1" });
c.AddSecurityDefinition("basic", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.Http,
Scheme = "basic",
In = ParameterLocation.Header,
Description = "Basic Authorization header using the Bearer scheme."
});
c.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "basic"
}
},
new string[] {}
}
});
});
services.AddCors(options => options.AddPolicy("CorsPolicy",
builder =>
{
builder.AllowAnyMethod().AllowAnyHeader()
.AllowCredentials();
}));
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
app.UseResponseCompression();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
// Enable middleware to serve generated Swagger as a JSON endpoint.
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS, etc.),
// specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
c.SwaggerEndpoint("/swagger/v1/swagger.json", "HojeTaPago API V1");
c.RoutePrefix = string.Empty;
});
app.UseHttpsRedirection();
app.UseRouting();
app.UseCors("CorsPolicy");
app.UseAuthentication();
app.UseAuthorization();
app.UseMiddleware<AuthenticationMiddleware>();
app.UseEndpoints(endpoints =>
{
endpoints.MapHub<NovoPedidoHub>("/novopedidohub");
endpoints.MapControllers();
});
}
}
Where im using the signalr
await _novoPedidoContext.Clients.All.SendAsync("NovoPedido", ListaComandaItem);
Client side - Blazor
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.
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();
services.AddServerSideBlazor();
services.AddBlazoredLocalStorage();
services.AddBootstrapCss();
services.AddTransient<HubConnectionBuilder>();
}
// 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.UseRouting();
app.UseEndpoints(endpoints =>
{
endpoints.MapBlazorHub();
endpoints.MapFallbackToPage("/_Host");
});
}
}
Where i call..
protected override async Task OnInitializedAsync()
{
DataService dataService = new DataService();
PedidosParaAceitar = new List<Comanda>(await dataService.BuscarComandasAbertas());
connection = _hubConnectionBuilder.WithUrl(dataService.servidor + "novopedidohub",
opt =>
{
opt.Transports = HttpTransportType.WebSockets;
opt.SkipNegotiation = true;
}).Build();
connection.On<List<ComandaItem>>("NovoPedido", async lista =>
{
var idEstabelecimento = await localStorage.GetItemAsync<int>("IdEstabelecimento");
if (lista.FirstOrDefault().Comanda.IdEstabelecimento == idEstabelecimento)
{
if (PedidosParaAceitar == null)
PedidosParaAceitar = new List<Comanda>();
if (PedidosParaAceitar.Count(x => x.Id == lista.FirstOrDefault().IdComanda) > 0)
foreach (var comandaitem in lista)
{
PedidosParaAceitar.FirstOrDefault(x => x.Id == lista.FirstOrDefault().IdComanda).ComandaItem.Add(comandaitem);
}
else
PedidosParaAceitar.Add(await dataService.BuscarComandaAberta(lista.FirstOrDefault().IdComanda));
StateHasChanged();
}
});
await connection.StartAsync();
}
You didn't specify in the tags if this was client-side (WASM) or server-side Blazor.
Looking at the question I noticed this line in ConfigureServices:
services.AddServerSideBlazor();
So you're attempting to use SignalR, a client-side communication library from the Server. In server-side Blazor all the C# code runs on the server. In this respect, SignalR is redundant since it's already being used by Blazor for communicating between the clients and the server.
By a very fortunate coincidence, I actually wrote an app to test this out recently. I created a server-side Blazor app, and wrote this service:
public class TalkService
{
public TalkService()
{
history = new List<string>();
}
public Action<string> OnChange { get; set; }
// inform all users of new message
public Task SendAsync(string message)
{
// add to history
history.Add(message);
// ensure only last 10 shown
if (history.Count > 10) history.RemoveAt(0);
OnChange.Invoke(message);
return Task.FromResult(0);
}
private readonly List<string> history;
public IReadOnlyList<string> GetHistory() => history;
}
I then registered it as a Singleton on the server (all clients use the same service)
in Startup.cs in the ConfigureServices() method:
services.AddSingleton<TalkService>();
Then rewrote Index.razor as follows:
#page "/"
#inject TalkService service
<p>Talk App started</p>
<p>Send a message: <input type="text"#bind="#message" />
<button class="btn btn-sm btn-primary" #onclick="Send" >Send</button>
</p>
#foreach (var m in messages)
{
<p>#m</p>
}
#code {
string message;
async Task Send()
{
if(!string.IsNullOrWhiteSpace(message))
await service.SendAsync(message);
message = string.Empty;
}
List<string> messages;
protected override void OnParametersSet()
{
// load history
messages = service.GetHistory().ToList();
// register for updates
service.OnChange += ChangeHandler;
}
protected void ChangeHandler(string message)
{
messages.Add(message);
InvokeAsync(StateHasChanged);
}
}
The talk service is a basic "chat" example of course. It's a Singleton so all pages/clients that reference it use the same instance. The service has a simple event OnChange that clients (like the Index page) can listen to for changes elsewhere.
SignalR isn't needed for this app since it's already "there" for server-side.
Demo App
The demo app also has a background service that generates time messages as well. I've pushed this to GitHub to help as a guide:
https://github.com/conficient/BlazorServerWithSignalR

Swagger Authorization with Okta configuration JWT token

I am trying to authorize the swagger api with the okta configuration using ASP.NET core 2.2.
Followed the instruction from this link
https://app.swaggerhub.com/help/enterprise/user-management/sso/okta
But quite not sure how can I do it.
Okta link
https://developer.okta.com/quickstart/?_ga=2.180885607.1554519477.1569975022-1481902663.1569975022#/angular/dotnet/aspnetcore
here is my Asp.net code
ConfigureSwagger(services);
protected virtual void ConfigureSwagger(IServiceCollection services)
{
// to view online help, goto ~/swagger/
services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
services.AddSwaggerGen(options =>
{
// add a custom operation filter which sets default values
options.OperationFilter<SwaggerDefaultValues>();
});
services.ConfigureSwaggerGen(options => { });
}
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
readonly IApiVersionDescriptionProvider provider;
/// <summary>
/// Initializes a new instance of the <see cref="ConfigureSwaggerOptions"/> class.
/// </summary>
/// <param name="provider">The <see cref="IApiVersionDescriptionProvider">provider</see> used to generate Swagger documents.</param>
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => this.provider = provider;
/// <inheritdoc />
public void Configure(SwaggerGenOptions options)
{
// add a swagger document for each discovered API version
// note: you might choose to skip or document deprecated API versions differently
foreach (var description in provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
options.OrderActionsBy(apiDesc => apiDesc.RelativePath);
options.IncludeXmlComments(Path.ChangeExtension(typeof(Startup).GetTypeInfo().Assembly.Location, "xml"));
options.DescribeAllEnumsAsStrings();
options.DescribeStringEnumsInCamelCase();
//options.AddSecurityDefinition("oauth2",
// new OAuth2Scheme
// {
// Type = "oauth2",
// Flow = "implicit",
// AuthorizationUrl = new Uri("/connect/authorize", UriKind.Relative).ToString(),
// Scopes = new Dictionary<string, string>
// {
// {"api1", "DEMO API"}
// }
// });
//options.AddSecurityRequirement(new[] { "oauth2", "api1" });
options.AddSecurityDefinition("oauth2",
new OpenApiSecurityScheme
{
Type = SecuritySchemeType.OAuth2,
Flows = new OpenApiOAuthFlows
{
Implicit = new OpenApiOAuthFlow
{
AuthorizationUrl = new Uri("/connect/authorize", UriKind.Relative),
Scopes = new Dictionary<string, string>
{
{Program.ResourceIdentifier, Program.ApplicationName}
}
}
}
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "oauth2"
}
},
new[] {"oauth2", Program.ResourceIdentifier }
}
});
options.EnableAnnotations();
//options.DocInclusionPredicate((docName, apiDesc) =>
//{
// if (!apiDesc.TryGetMethodInfo(out MethodInfo methodInfo)) return false;
// var versions = methodInfo.DeclaringType
// .GetCustomAttributes(true)
// .OfType<ApiVersionAttribute>()
// .SelectMany(attr => attr.Versions);
// return versions.Any(v => $"v{v.ToString()}" == docName);
//});
}
static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo
{
Title = Program.ApplicationName,
Version = $"v{description.ApiVersion}",
Description = "A sample application with Swagger, Swashbuckle, and API versioning."
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
public static void UseSwaggerMiddleware(this IApplicationBuilder app, IApiVersionDescriptionProvider provider)
{
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
//OAuth2
c.OAuthClientId("{clientId}");
//c.OAuth2RedirectUrl("");
//c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
c.OAuthClientSecret("{ClientSecret}");
c.OAuthAppName("{AppName}");
c.OAuthScopeSeparator("openid profile email");
c.OAuthAdditionalQueryStringParams(new Dictionary<string, string>
{
{ "response_type","token"}
});
});
}
Errors
Hide
Auth error
{"state":"VGh1IE9jdCAwMyAyMDE5IDE1OjI4OjEyIEdNVCsxMDAwIChBVVMgRWFzdGVybiBTdGFuZGFyZCBUaW1lKQ==","error":"unsupported_response_type","error_description":"The+response+type+is+not+supported+by+the+authorization+server.+Configured+response+types:+[id_token,+code]."}
How do I configure with swagger authorization client with JWT token.
Finally found the solution
Need to do this configuration on asp.net core
public static void UseSwaggerMiddleware(this IApplicationBuilder app, IApiVersionDescriptionProvider provider, IConfiguration Configuration)
{
app.UseSwagger();
// Enable middleware to serve swagger-ui (HTML, JS, CSS etc.), specifying the Swagger JSON endpoint.
app.UseSwaggerUI(c =>
{
// build a swagger endpoint for each discovered API version
foreach (var description in provider.ApiVersionDescriptions)
{
c.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
}
//c.SwaggerEndpoint("/swagger/v2/swagger.json", "DEMO Api v2");
//c.SwaggerEndpoint("/swagger/v1/swagger.json", "DEMO Api v1");
//OAuth2
var OktaConfig = new OktaConfig();
Configuration.GetSection("OktaConfig").Bind(OktaConfig);
c.OAuthClientId(OktaConfig.ClientId);
c.OAuth2RedirectUrl($"{OktaConfig.RedirectUrl}/swagger/oauth2-redirect.html");
c.OAuthUseBasicAuthenticationWithAccessCodeGrant();
c.OAuthClientSecret(OktaConfig.ClientSecret);
c.OAuthAppName(OktaConfig.ClientName);
c.OAuthScopeSeparator($"openid profile email {Program.ResourceIdentifier}");
c.OAuthAdditionalQueryStringParams(new Dictionary<string, string>
{
{ "response_type","token"},
{ "nonce", "nonce" }
});
//c.ConfigObject.DeepLinking = true;
});
}
And need to add policies and rule

How to throwing 404 on bad .NET Core API route?

I have a .NET Core web app which has an API. I've defined an Middleware class based on this answer like so:
public class ErrorHandlingMiddleware
{
private readonly RequestDelegate next;
private readonly ILogger logger;
public ErrorHandlingMiddleware(RequestDelegate next,
ILoggerFactory loggerFactory)
{
this.next = next;
logger = loggerFactory.CreateLogger<ErrorHandlingMiddleware>();
}
public async Task Invoke(HttpContext context)
{
try
{
await next(context);
}
catch (Exception ex)
{
logger.LogError(0, ex, "An unhandled exception has occurred: " + ex.StackTrace);
await HandleExceptionAsync(context, ex);
}
}
private static Task HandleExceptionAsync(HttpContext context, Exception exception)
{
var code = HttpStatusCode.InternalServerError;
var message = exception.Message;
if (exception is BadRequestException)
{
code = HttpStatusCode.BadRequest;
}
else if (exception is NotFoundException)
{
code = HttpStatusCode.NotFound;
}
else if (exception is NotAuthorizedException)
{
code = HttpStatusCode.Forbidden;
}
else if (exception is NotAuthenticatedException)
{
code = HttpStatusCode.Unauthorized;
}
else
{
message = "An unexpected error occurred.";
}
var result = JsonConvert.SerializeObject(new { error = message });
context.Response.ContentType = "application/json";
context.Response.StatusCode = (int)code;
return context.Response.WriteAsync(result);
}
}
The error handling only handles when an exception is thrown in code. A bad route does not throw an exception. The problem is that if I try to access a non-existent API route - that is, one that follows the API route convention and starts with "/api/adfasdf" - the API returns HTML (or the error page or the home page, I forget).
I've received some suggestions to check the context.Response.StatusCode after await next(context); executes, but it's 200.
How can I configure my web app such that it recognizes a bad API route and returns a 404?
UPDATE
Here is where/when I load the middleware in my Startup class:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime, IOptions<OidcConfig> oidcConfigOptions)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
// Add Serilog to the logging pipeline
loggerFactory.AddSerilog();
app.UseMiddleware<ErrorHandlingMiddleware>();
if (env.IsLocal())
{
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions
{
HotModuleReplacement = true
});
}
var oidcConfig = oidcConfigOptions.Value;
// Configure the app to use Jwt Bearer Authentication
app.UseJwtBearerAuthentication(new JwtBearerOptions
{
AutomaticAuthenticate = true,
AutomaticChallenge = true,
Authority = oidcConfig.GetAuthority(),
Audience = oidcConfig.ResourceAppId,
TokenValidationParameters = new TokenValidationParameters
{
RequireExpirationTime = true,
RequireSignedTokens = true,
ValidateAudience = true,
ValidIssuer = oidcConfig.GetIssuer(),
ValidateIssuer = true,
ValidateActor = false,
ValidateLifetime = true,
ValidateIssuerSigningKey = true
},
});
app.UseSiteIdClaimInjection();
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" });
});
appLifetime.ApplicationStopped.Register(() => this.ApplicationContainer.Dispose());
}
For posterity, the reason I was getting a 200 as #Nkosi helped uncover had to do with an MVC route definition in the Startup class. This came in automatically from https://github.com/aspnet/JavaScriptServices.
The solution was to change my route config to the following:
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.MapWhen(x => !x.Request.Path.Value.StartsWith("/api"), builder =>
{
builder.UseMvc(routes =>
{
routes.MapSpaFallbackRoute(
name: "spa-fallback",
defaults: new { controller = "Home", action = "Index" });
});
});
Referencing ASP.NET Core Middleware Fundamentals - Ordering
The order that middleware components are added in the Configure method
defines the order in which they are invoked on requests, and the
reverse order for the response. This ordering is critical for
security, performance, and functionality.
The Configure method (shown below) adds the following middleware
components:
Exception/error handling
Static file server
Authentication
MVC
C#
public void Configure(IApplicationBuilder app)
{
app.UseExceptionHandler("/Home/Error"); // Call first to catch exceptions
// thrown in the following middleware.
app.UseStaticFiles(); // Return static files and end pipeline.
app.UseIdentity(); // Authenticate before you access
// secure resources.
app.UseMvcWithDefaultRoute(); // Add MVC to the request pipeline.
}
In the code above, UseExceptionHandler is the first middleware
component added to the pipeline—therefore, it catches any exceptions
that occur in later calls.
Based on code provided in OP and quoted documentation I would suggest adding your exception earlier or first to the pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IApplicationLifetime appLifetime) {
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
loggerFactory.AddSerilog();
app.UseMiddleware<ErrorHandlingMiddleware>(); // Call first to catch exceptions
// thrown in the following middleware.
if (env.IsLocal()) {
app.UseWebpackDevMiddleware(new WebpackDevMiddlewareOptions { HotModuleReplacement = true });
}
//Bunch of other stuff
}
UPDATE Based on comments.
I suspect that one of the middleware further down the pipeline is causing this issue. try removing them one by one and checking if you get the same behavior in order to narrow down which one is the culprit.
Similar to above answer, We are using this in our Angular and ASP.NET MVC Core project:
public virtual void Configure(IHostingEnvironment environment, IApplicationBuilder app)
{
// configurations...
app.UseMvc(routes =>
{
routes.MapRoute("default", "{controller=Home}/{action=Index}/{id?}");
});
app.MapWhen(o => !o.Request.Path.Value.StartsWith("/api"), builder =>
{
builder.UseMvc(routes =>
{
routes.MapRoute("spa-fallback", "{*anything}", new { controller = "Home", action = "Index" });
});
});
}

OpenIDDict in asp.net core error " The OpenID Connect request cannot be retrieved"

System.InvalidOperationException: The OpenID Connect request cannot be
retrieved from the ASP.NET context. Make sure that
'app.UseOpenIddict()' is called before 'app.UseMvc()' and that the
action route corresponds to the endpoint path registered via
'services.AddOpenIddict().Enable[...]Endpoint(...)'. at
OpenIddict.Mvc.OpenIddictModelBinder.BindModelAsync(ModelBindingContext
context)
MyStartup.cs
public void ConfigureServices(IServiceCollection services)
{
services.AddApplicationInsightsTelemetry(Configuration);
services.AddDbContext<ApplicationUserDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddIdentity<ApplicationUser, ApplicationRole>()
.AddEntityFrameworkStores<ApplicationUserDbContext>()
.AddDefaultTokenProviders();
services.AddMvc();
.AddMvcBinders()
.EnableAuthorizationEndpoint("/connect/authorize")
.EnableLogoutEndpoint("/connect/logout")
.EnableTokenEndpoint("/connect/token")
.EnableUserinfoEndpoint("/Account/Userinfo")
.AllowAuthorizationCodeFlow()
.AllowPasswordFlow()
.AllowRefreshTokenFlow()
.RequireClientIdentification()
// During development, you can disable the HTTPS requirement.
.DisableHttpsRequirement()
.AddEphemeralSigningKey();
services.AddTransient<IEmailSender, AuthMessageSender>();
services.AddTransient<ISmsSender, AuthMessageSender>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole(Configuration.GetSection("Logging"));
loggerFactory.AddDebug();
app.UseApplicationInsightsRequestTelemetry();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
app.UseDatabaseErrorPage();
app.UseBrowserLink();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseApplicationInsightsExceptionTelemetry();
app.UseStaticFiles();
app.UseCsp(options => options.DefaultSources(directive => directive.Self())
.ImageSources(directive => directive.Self()
.CustomSources("*"))
.ScriptSources(directive => directive.Self()
.UnsafeInline())
.StyleSources(directive => directive.Self()
.UnsafeInline()));
app.UseXContentTypeOptions();
app.UseXfo(options => options.Deny());
app.UseXXssProtection(options => options.EnabledWithBlockMode());
app.UseIdentity();
// Add a middleware used to validate access
// tokens and protect the API endpoints.
app.UseOAuthValidation();
app.UseGoogleAuthentication(new GoogleOptions
{
});
app.UseStatusCodePagesWithReExecute("/error");
app.UseOpenIddict();
app.UseMvcWithDefaultRoute();
}
Update
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using AspNet.Security.OpenIdConnect.Extensions;
using AspNet.Security.OpenIdConnect.Server;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http.Authentication;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Mvc.Server.Models;
using Mvc.Server.ViewModels.Authorization;
using Mvc.Server.ViewModels.Shared;
using OpenIddict;
public class AuthorizationController : Controller {
private readonly OpenIddictApplicationManager<OpenIddictApplication> _applicationManager;
private readonly SignInManager<ApplicationUser> _signInManager;
private readonly UserManager<ApplicationUser> _userManager;
public AuthorizationController(
OpenIddictApplicationManager<OpenIddictApplication> applicationManager,
SignInManager<ApplicationUser> signInManager,
UserManager<ApplicationUser> userManager) {
_applicationManager = applicationManager;
_signInManager = signInManager;
_userManager = userManager;
}
// Note: to support interactive flows like the code flow,
// you must provide your own authorization endpoint action:
[Authorize, HttpGet, Route("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request) {
// Retrieve the application details from the database.
var application = await _applicationManager.FindByClientIdAsync(request.ClientId);
if (application == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.InvalidClient,
ErrorDescription = "Details concerning the calling client application cannot be found in the database"
});
}
// Flow the request_id to allow OpenIddict to restore
// the original authorization request from the cache.
return View(new AuthorizeViewModel {
ApplicationName = application.DisplayName,
RequestId = request.RequestId,
Scope = request.Scope
});
}
[Authorize, HttpPost("~/connect/authorize/accept"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
[Authorize, HttpPost("~/connect/authorize/deny"), ValidateAntiForgeryToken]
public IActionResult Deny() {
// Notify OpenIddict that the authorization grant has been denied by the resource owner
// to redirect the user agent to the client application using the appropriate response_mode.
return Forbid(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Note: the logout action is only useful when implementing interactive
// flows like the authorization code flow or the implicit flow.
[HttpGet("~/connect/logout")]
public IActionResult Logout(OpenIdConnectRequest request) {
// Flow the request_id to allow OpenIddict to restore
// the original logout request from the distributed cache.
return View(new LogoutViewModel {
RequestId = request.RequestId
});
}
[HttpPost("~/connect/logout"), ValidateAntiForgeryToken]
public async Task<IActionResult> Logout() {
// Ask ASP.NET Core Identity to delete the local and external cookies created
// when the user agent is redirected from the external identity provider
// after a successful authentication flow (e.g Google or Facebook).
await _signInManager.SignOutAsync();
// Returning a SignOutResult will ask OpenIddict to redirect the user agent
// to the post_logout_redirect_uri specified by the client application.
return SignOut(OpenIdConnectServerDefaults.AuthenticationScheme);
}
// Note: to support non-interactive flows like password,
// you must provide your own token endpoint action:
[HttpPost("~/connect/token")]
[Produces("application/json")]
public async Task<IActionResult> Exchange(OpenIdConnectRequest request) {
if (request.IsPasswordGrantType()) {
var user = await _userManager.FindByNameAsync(request.Username);
if (user == null) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the user is allowed to sign in.
if (!await _signInManager.CanSignInAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Reject the token request if two-factor authentication has been enabled by the user.
if (_userManager.SupportsUserTwoFactor && await _userManager.GetTwoFactorEnabledAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The specified user is not allowed to sign in."
});
}
// Ensure the user is not already locked out.
if (_userManager.SupportsUserLockout && await _userManager.IsLockedOutAsync(user)) {
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
// Ensure the password is valid.
if (!await _userManager.CheckPasswordAsync(user, request.Password)) {
if (_userManager.SupportsUserLockout) {
await _userManager.AccessFailedAsync(user);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.InvalidGrant,
ErrorDescription = "The username/password couple is invalid."
});
}
if (_userManager.SupportsUserLockout) {
await _userManager.ResetAccessFailedCountAsync(user);
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
return BadRequest(new OpenIdConnectResponse {
Error = OpenIdConnectConstants.Errors.UnsupportedGrantType,
ErrorDescription = "The specified grant type is not supported."
});
}
private async Task<AuthenticationTicket> CreateTicketAsync(OpenIdConnectRequest request, ApplicationUser user) {
// Create a new ClaimsPrincipal containing the claims that
// will be used to create an id_token, a token or a code.
var principal = await _signInManager.CreateUserPrincipalAsync(user);
// Note: by default, claims are NOT automatically included in the access and identity tokens.
// To allow OpenIddict to serialize them, you must attach them a destination, that specifies
// whether they should be included in access tokens, in identity tokens or in both.
foreach (var claim in principal.Claims) {
// In this sample, every claim is serialized in both the access and the identity tokens.
// In a real world application, you'd probably want to exclude confidential claims
// or apply a claims policy based on the scopes requested by the client application.
claim.SetDestinations(OpenIdConnectConstants.Destinations.AccessToken,
OpenIdConnectConstants.Destinations.IdentityToken);
}
// Create a new authentication ticket holding the user identity.
var ticket = new AuthenticationTicket(
principal, new AuthenticationProperties(),
OpenIdConnectServerDefaults.AuthenticationScheme);
// Set the list of scopes granted to the client application.
// Note: the offline_access scope must be granted
// to allow OpenIddict to return a refresh token.
ticket.SetScopes(new[] {
OpenIdConnectConstants.Scopes.OpenId,
OpenIdConnectConstants.Scopes.Email,
OpenIdConnectConstants.Scopes.Profile,
OpenIdConnectConstants.Scopes.OfflineAccess,
OpenIddictConstants.Scopes.Roles
}.Intersect(request.GetScopes()));
return ticket;
}
}
Changed:
[Authorize, HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request) {
// Retrieve the profile of the logged in user.
var user = await _userManager.GetUserAsync(User);
if (user == null) {
return View("Error", new ErrorViewModel {
Error = OpenIdConnectConstants.Errors.ServerError,
ErrorDescription = "An internal error has occurred"
});
}
// Create a new authentication ticket.
var ticket = await CreateTicketAsync(request, user);
// Returning a SignInResult will ask OpenIddict to issue the appropriate access/identity tokens.
return SignIn(ticket.Principal, ticket.Properties, ticket.AuthenticationScheme);
}
OpenIddict used to allow "subroutes" like /connect/authorize/accept or /connect/authorize/deny to be recognized as valid authorization endpoint paths when /connect/authorize was specified, but this feature was removed recently.
With the latest OpenIddict bits, you're encouraged to use the same route template for all your authorization endpoint actions.
[Authorize, HttpGet("~/connect/authorize")]
public async Task<IActionResult> Authorize(OpenIdConnectRequest request)
{
// ...
}
[Authorize, FormValueRequired("submit.Accept")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public async Task<IActionResult> Accept(OpenIdConnectRequest request)
{
// ...
}
[Authorize, FormValueRequired("submit.Deny")]
[HttpPost("~/connect/authorize"), ValidateAntiForgeryToken]
public IActionResult Deny()
{
// ...
}
You can use Orchard's [FormValueRequired] approach to discriminate your actions:
public sealed class FormValueRequiredAttribute : ActionMethodSelectorAttribute
{
private readonly string _name;
public FormValueRequiredAttribute(string name)
{
_name = name;
}
public override bool IsValidForRequest(RouteContext context, ActionDescriptor action)
{
if (string.Equals(context.HttpContext.Request.Method, "GET", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "HEAD", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "DELETE", StringComparison.OrdinalIgnoreCase) ||
string.Equals(context.HttpContext.Request.Method, "TRACE", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (string.IsNullOrEmpty(context.HttpContext.Request.ContentType))
{
return false;
}
if (!context.HttpContext.Request.ContentType.StartsWith("application/x-www-form-urlencoded", StringComparison.OrdinalIgnoreCase))
{
return false;
}
return !string.IsNullOrEmpty(context.HttpContext.Request.Form[_name]);
}
}
Don't forget to also update your submit buttons:
<input class="btn btn-lg btn-success" name="submit.Accept" type="submit" value="Yes" />
<input class="btn btn-lg btn-danger" name="submit.Deny" type="submit" value="No" />

How to write in memory integration test for a WebApi secured by OpenId Connect, cookie authentication and hosted by NancyFx and Owin

I have a WabApi project which uses Owin and NancyFX. The api is secured by OpenId Connect and cookie authentication.
I have to write some in memory integration tests using HttpClient, which is quite easy as long as you don't try to use authentication based on OpenId Connect and cookie.
Does anyone know how to prepare a proper authentication cookie for HttpClient to let it connect with WebApi as authenticated user?
Currently I'm able to do some http calls to get the proper access token, id token etc. from OpenId Connect provider (implemented by IdentityServer v3), but I have no idea how to prepare authentication cookie for HttpClient.
PS: I uses Hybrid flow for OpenId Connect
Below you can find some of my files.
Server project:
AppStartup for WebApi:
The server application hosts WebApi and OpenId Connect provider (IdentityServer v3) at the same time, so its app starup looks like this:
public class ServerAppStartup
{
public static void Configuration(IAppBuilder app)
{
app.Map("/identity", idsrvApp =>
{
var factory = new IdentityServerServiceFactory {...};
idsrvApp.UseIdentityServer(new IdentityServerOptions
{
SiteName = "server app",
SigningCertificate = ...,
RequireSsl = false,
Factory = factory,
AuthenticationOptions = new AuthenticationOptions {
RememberLastUsername = true
},
EnableWelcomePage = false
});
});
app.SetDefaultSignInAsAuthenticationType("ClientCookie");
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = "ClientCookie",
CookieName = CookieAuthenticationDefaults.CookiePrefix + "ClientCookie",
ExpireTimeSpan = TimeSpan.FromMinutes(5)
});
app.UseOpenIdConnectAuthentication(new OpenIdConnectAuthenticationOptions
{
AuthenticationMode = AuthenticationMode.Active,
AuthenticationType = OpenIdConnectAuthenticationDefaults.AuthenticationType,
SignInAsAuthenticationType = app.GetDefaultSignInAsAuthenticationType(),
Authority = options.BaseUrl+ "identity",
ClientId = options.ClientId,
RedirectUri = options.RedirectUri,
PostLogoutRedirectUri = options.PostLogoutRedirectUri,
ResponseType = "code id_token",
Scope = "openid profile offline_access",
Notifications = new OpenIdConnectAuthenticationNotifications
{
AuthorizationCodeReceived = async n =>
{
/* stuff to get ACCESS TOKEN from CODE TOKEN */
},
RedirectToIdentityProvider = n =>
{
if (n.ProtocolMessage.RequestType == OpenIdConnectRequestType.LogoutRequest)
{
var idTokenHint = n.OwinContext.Authentication.User.FindFirst("id_token");
if (idTokenHint != null)
{
n.ProtocolMessage.IdTokenHint = idTokenHint.Value;
}
}
return Task.FromResult(0);
}
}
}
JwtSecurityTokenHandler.InboundClaimTypeMap = new Dictionary<string, string>();
app.UseNancy();
app.UseStageMarker(PipelineStage.MapHandler);
}
Sample nancy module (something like controller in MVC or WebApi):
using System;
using Nancy.ModelBinding;
using Nancy.Security;
namespace Server.Modules
{
public class UsersModule : BaseModule
{
public UsersModule() : base("/users")
{
Get["/getall"] = parameters =>
{
this.RequiresMSOwinAuthentication();
...
return ...;
};
}
}
}
Integration test project:
Test server to let me run WebApi in memory:
public class TestServer: IDisposable
{
private Func<IDictionary<string, object>, Task> _appFunc;
public static CookieContainer CookieContainer;
public Uri BaseAddress { get; set; }
// I uses OwinHttpMessageHandler becaouse it can handle http redirections
public OwinHttpMessageHandler Handler { get; private set; }
public HttpClient HttpClient => new HttpClient(Handler) { BaseAddress = BaseAddress };
public static TestServer Create()
{
CookieContainer = new CookieContainer();
var result = new TestServer();
var appBuilder = new AppBuilder();
appBuilder.Properties["host.AppName"] = "WebApi server";
/* Use configuration of server app */
ServerAppStartup.Configuration(appBuilder);
result._appFunc = appBuilder.Build();
result.Handler = new OwinHttpMessageHandler(result._appFunc)
{
AllowAutoRedirect = true,
AutoRedirectLimit = 1000,
CookieContainer = CookieContainer,
UseCookies = true
};
return result;
}
public void Dispose()
{
Handler.Dispose();
GC.SuppressFinalize(this);
}
}
Sample test:
namespace ServerSpec.Specs.Users
{
public class GetAllUsersSpec
{
private TestServer _server;
public GetAllUsersSpec(){
server = TestServer.create();
}
[Fact]
public void should_return_all_users()
{
/* here I will get error because http client or rather its cookie handler has no authentication cookie */
var users = Get("/users/getall");
...
}
public TResponse Get<TResponse>(string urlFragment)
{
var client = server.HttpClient();
var httpResponse = client.GetAsync(urlFragment).Result;
httpResponse.EnsureSuccessStatusCode();
return httpResponse.Content.ReadAsAsync<TResponse>().Result;
}
}
}
Check out the unit and integration tests in this project:
https://github.com/IdentityModel/IdentityModel.Owin.PopAuthentication
It shows doing in-memory integration testing with IdentityServer in one pipeline, and a (fake) web api in another pipeline that accepts tokens.

Resources