Stop Expired Access Token from retrieving data from resource Server - asp.net-core-2.0

I have been fiddling around with IDS 4 and I have a small problem. I set my token life times to about 15 seconds and even though they are expired I can still retrieve date from my resource server. If I remove the token from my header on the client call I get a 401 error.
Client
[Authorize]
public async Task<ActionResult> Shouts()
{
var accessToken = await HttpContext.GetTokenAsync("access_token");
var tokenh = new JwtSecurityTokenHandler();
var jwtSecurityToken= tokenh.ReadJwtToken(accessToken);
var val = jwtSecurityToken.ValidTo;
using (var client = new HttpClient())
{
client.DefaultRequestHeaders.Authorization = new System.Net.Http.Headers.AuthenticationHeaderValue("Bearer", accessToken);
var rawResponse = await client.GetAsync("http://localhost:5002/api/Values/Get");
if (rawResponse.StatusCode == System.Net.HttpStatusCode.Unauthorized)
{
var refreshSucc = await this.RefreshTokensAsync(this.HttpContext);
if (!refreshSucc)
{
var authServerInfo = await this.GetAuthenticationServerInfo();
return Redirect(authServerInfo.AuthorizeEndpoint);
}
}
var response = await (rawResponse).Content.ReadAsStringAsync();
var data = JsonConvert.DeserializeObject<List<String>>(response);
return View("Shouts", data);
}
}
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)
{
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
services.AddAuthentication(options =>
{
options.DefaultScheme = "Cookies";
options.DefaultChallengeScheme = "oidc";
})
.AddCookie("Cookies",o=>o.LogoutPath="/home/logout")
.AddOpenIdConnect("oidc", opt =>
{
opt.SignInScheme = "Cookies";
opt.Authority = "http://localhost:5000";
opt.RequireHttpsMetadata = false;
opt.ClientId = "AuthTest_Code";
opt.ClientSecret = "secret";
opt.ResponseType = "id_token code";
opt.Scope.Add("TestAPI");
opt.Scope.Add("offline_access");
opt.Scope.Add("email");
opt.GetClaimsFromUserInfoEndpoint = true;
opt.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.UseBrowserLink();
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
}
app.UseStaticFiles();
app.UseAuthentication();
app.UseMvc(routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
}
}
Auth Server
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "AuthTest_Code",
ClientSecrets=new []{new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Hybrid,
AllowedScopes = new List<string>
{
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"TestAPI"
},
AllowAccessTokensViaBrowser=true,
AllowOfflineAccess=true,
RedirectUris = new [] { "http://localhost:5001/signin-oidc" },
PostLogoutRedirectUris={ "http://localhost:5001/signout-callback-odic"},
RequireConsent=false,
AccessTokenLifetime=15,
AbsoluteRefreshTokenLifetime=15
}
};
}
public class Startup
{
public IHostingEnvironment Environment { get; }
public Startup(IHostingEnvironment environment)
{
Environment = environment;
}
// 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)
{
var isServer = services.AddIdentityServer()
.AddSigningCredential(new X509Certificate2(#"C:\OpenSSLCerts\Authenticate.pfx", "Password1"))
.AddInMemoryApiResources(TestConfig.GetApis())
.AddInMemoryClients(TestConfig.GetClients())
.AddInMemoryIdentityResources(TestConfig.GetIdentityResources())
.AddTestUsers(TestConfig.GetUsers());
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 (Environment.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
}
Resource Server
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
namespace ResourceAPI
{
public class Startup
{
// 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.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.ApiName = "TestAPI";
});
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
app.UseAuthentication();
app.UseMvc();
}
}
}
[Route("api/[controller]")]
public class ValuesController:ControllerBase
{
[HttpGet]
[Route("Get")]
[Authorize]
public async Task<IActionResult> Get()
{
var username = User.Claims.FirstOrDefault(t => t.Type == "email")?.Value;
return Ok(new string[] { "value1", "value2" });
}
}
Access Token:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjQ0Q0Q4NjFEQjA0MzdEMUM4NUY2REU0MTIyMkFDOEIwMTE3M0Q2MTAiLCJ0eXAiOiJKV1QiLCJ4NXQiOiJSTTJHSGJCRGZSeUY5dDVCSWlySXNCRnoxaEEifQ.eyJuYmYiOjE1NDIxMjc4OTQsImV4cCI6MTU0MjEyNzkwOSwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjpbImh0dHA6Ly9sb2NhbGhvc3Q6NTAwMC9yZXNvdXJjZXMiLCJUZXN0QVBJIl0sImNsaWVudF9pZCI6IkF1dGhUZXN0X0NvZGUiLCJzdWIiOiIxIiwiYXV0aF90aW1lIjoxNTQyMTI3ODkzLCJpZHAiOiJsb2NhbCIsImVtYWlsIjoiRGF2aWRAQUJldHRlckxpZmUuY28uemEiLCJzY29wZSI6WyJvcGVuaWQiLCJwcm9maWxlIiwiZW1haWwiLCJUZXN0QVBJIiwib2ZmbGluZV9hY2Nlc3MiXSwiYW1yIjpbInB3ZCJdfQ.hNskjZz3IBqPg_5T0xVwYEP5RukcMt3l019PRp74vNBkiEr6FLvBADa_eylhNGA8qDd7SwyDkq6APaKxaNt0YybAChZvFW6pzLlfknVVHM1vuN7PDOX9PNGhFK9kSONBypXo6JqV3epactsmJvhr3FZxBSufUDRyV4j_YY3O8TCjJf_5UORc_3ox9clNdjt-Vx-BlcDjVbpBw2P76F3pq2IPPDM139H4qYcyaTzSlEbFd3EdVwO6O85bWM1G8yQ9zQAUk23It29oHxTtwsi5Wg4Zpmt7K7AlvKjKxKoxw_Y32QdBkGY9xU_KXOn4h0WJV3LG-InZc7BveLGHq6ncaQ

Try setting options.JwtValidationClockSkew property to zero in your web api(resource server)
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.JwtValidationClockSkew = TimeSpan.Zero;
options.Authority = "https://localhost:44323";
options.RequireHttpsMetadata = false;
options.ApiName = "api1";
});
There is a clock skew in the Microsoft JWT validation middleware. It is set by default to 5 mins. And it's suggest to leave it as default (300 seconds/5 minutes).

This is most likely due to the default 5 minute (if memory serves) clock skew allowance. It is however possible to override this setting in IDS4 and the bearer token middleware.

Related

SignalR with Azure AD (Blazor Server chat app)

https://learn.microsoft.com/en-us/azure/azure-signalr/signalr-tutorial-build-blazor-server-chat-app
How do I get this to work with Azure AD activated? It works perfect when I run in locally in visual studio, but when deployed it will not work with Azure AD, only if I remove Azure AD it works.
This is the error message when deployed and after clicking the button "Chat!" next to username textbox:
"ERROR: Failed to start chat client: Response status code does not indicate success: 403 (Forbidden)."
(I have found other threads like this Blazor Server SignalR Chat works on Local, not on Azure but no solution)
//Program.cs
using BlazorApp6ADChat;
using BlazorApp6ADChat.Data;
using BlazorChat;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.Identity.Web;
using Microsoft.Identity.Web.UI;
var builder = WebApplication.CreateBuilder(args);
// Add services to the container.
builder.Services.AddAuthentication(OpenIdConnectDefaults.AuthenticationScheme)
.AddMicrosoftIdentityWebApp(builder.Configuration.GetSection("AzureAd"));
builder.Services.AddControllersWithViews()
.AddMicrosoftIdentityUI();
builder.Services.AddAuthorization(options =>
{
// By default, all incoming requests will be authorized according to the default policy
options.FallbackPolicy = options.DefaultPolicy;
});
builder.Services.AddRazorPages();
builder.Services.AddServerSideBlazor()
.AddMicrosoftIdentityConsentHandler();
builder.Services.AddSingleton<WeatherForecastService>();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (!app.Environment.IsDevelopment())
{
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.MapControllers();
app.MapBlazorHub();
app.MapFallbackToPage("/_Host");
app.MapHub<BlazorChatSampleHub>(BlazorChatSampleHub.HubUrl);
app.UseAuthentication();
app.UseAuthorization();
app.Run();
//appsettings.json
{
"AzureAd": {
"Instance": "https://login.microsoftonline.com/",
"Domain": "xxx.onmicrosoft.com",
"TenantId": "xxx",
"ClientId": "xxx",
"CallbackPath": "/signin-oidc"
},
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}
Not sure if this will help. This is how I wire up a WebAssembly Host (server) with a SignalR Hub.
services.TryAddEnumerable(
ServiceDescriptor.Singleton<IPostConfigureOptions<JwtBearerOptions>,
ConfigureJwtBearerOptions>());
public class ConfigureJwtBearerOptions : IPostConfigureOptions<JwtBearerOptions>
{
public void PostConfigure(string name, JwtBearerOptions options)
{
var originalOnMessageReceived = options.Events.OnMessageReceived;
options.Events.OnMessageReceived = async context =>
{
await originalOnMessageReceived(context);
if (string.IsNullOrEmpty(context.Token))
{
var accessToken = context.Request.Query["access_token"];
var requestPath = context.HttpContext.Request.Path;
var endPoint = $"/chathub";
if (!string.IsNullOrEmpty(accessToken) &&
requestPath.StartsWithSegments(endPoint))
{
context.Token = accessToken;
}
}
};
}
}
I posted a solution here: Microsoft Learn
Basically:
Manually getting the cookies in the host.cshtml page
Passing the Cookie collection to the app.razor so I can create a cascading parameter
Retrieving the parameter and manually populating the cookie container when instantiating the SignalR client (you may have seen this code on the web, but without steps 1 and 2 it will not work on Azure only in IIS Express)
Program.cs
public class Program
{
public static void Main(string[] args)
{
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddAutoMapper(typeof(Program).Assembly);
builder.Services.AddHttpClient();
builder.Services.AddHttpContextAccessor();
builder.Services.AddScoped<HttpContextAccessor>();
builder.Services.AddResponseCompression(opts =>
{
opts.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat(
new[] { "application/octet-stream" });
});
var app = builder.Build();
app.UseResponseCompression();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.MapBlazorHub();
app.MapHub<ChatHub>("/chathub");
app.MapFallbackToPage("/_Host");
app.Run();
}
}
_Host.cshtml:
<body>
#{
var CookieCollection = HttpContext.Request.Cookies;
Dictionary<string, string> Cookies = new Dictionary<string, string>();
foreach (var cookie in CookieCollection)
{
Cookies.Add(cookie.Key, cookie.Value);
}
}
<component type="typeof(App)" render-mode="ServerPrerendered" param-Cookies="Cookies" />
app.razor:
<CascadingValue Name="Cookies" Value="Cookies">
<CascadingAuthenticationState>
<Router AppAssembly="#typeof(App).Assembly">
<Found Context="routeData">
<AuthorizeRouteView RouteData="#routeData" DefaultLayout="#typeof(MainLayout)" />
<FocusOnNavigate RouteData="#routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="#typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</CascadingAuthenticationState>
</CascadingValue>
#code {
[Parameter] public Dictionary<string, string> Cookies { get; set; }
}
Index.razor:
#code {
#nullable disable
[CascadingParameter(Name = "Cookies")] public Dictionary<string, string> Cookies { get; set; }
System.Security.Claims.ClaimsPrincipal CurrentUser;
private HubConnection hubConnection;
private List<string> messages = new List<string>();
private string userInput;
private string messageInput;
private string strError = "";
protected override async Task OnInitializedAsync()
{
try
{
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
CurrentUser = authState.User;
// ** SignalR Chat
try
{
hubConnection = new HubConnectionBuilder()
.WithUrl(Navigation.ToAbsoluteUri("/chathub"), options =>
{
options.UseDefaultCredentials = true;
var cookieCount = Cookies.Count();
var cookieContainer = new CookieContainer(cookieCount);
foreach (var cookie in Cookies)
cookieContainer.Add(new Cookie(
cookie.Key,
WebUtility.UrlEncode(cookie.Value),
path: "/",
domain: Navigation.ToAbsoluteUri("/").Host));
options.Cookies = cookieContainer;
foreach (var header in Cookies)
options.Headers.Add(header.Key, header.Value);
options.HttpMessageHandlerFactory = (input) =>
{
var clientHandler = new HttpClientHandler
{
PreAuthenticate = true,
CookieContainer = cookieContainer,
UseCookies = true,
UseDefaultCredentials = true,
};
return clientHandler;
};
})
.WithAutomaticReconnect()
.Build();
hubConnection.On<string, string>("ReceiveMessage", (user, message) =>
{
var encodedMsg = $"{user}: {message}";
messages.Add(encodedMsg);
InvokeAsync(StateHasChanged);
});
await hubConnection.StartAsync();
}
catch(Exception ex)
{
strError = ex.Message;
}
}
catch
{
// do nothing if this fails
}
}
// ** SignalR Chat
private async Task Send()
{
if (hubConnection is not null)
{
await hubConnection.SendAsync("SendMessage", messageInput);
}
}
public bool IsConnected =>
hubConnection?.State == HubConnectionState.Connected;
public async ValueTask DisposeAsync()
{
if (hubConnection is not null)
{
await hubConnection.DisposeAsync();
}
}
}
ChatHub.cs:
[Authorize]
public class ChatHub : Hub
{
public async override Task OnConnectedAsync()
{
if (this.Context.User?.Identity?.Name != null)
{
await Clients.All.SendAsync(
"broadcastMessage",
"_SYSTEM_",
$"{Context.User.Identity.Name} JOINED");
}
}
public async Task SendMessage(string message)
{
if (this.Context.User?.Identity?.Name != null)
{
await Clients.All.SendAsync(
"ReceiveMessage",
this.Context.User.Identity.Name,
message);
}
}
}

Signal R Handshake failed with Azure Service Fabric

We have a Flutter Dart Client application that has to be integrated with Service Fabric Microservice.
Client-side code is not able to establish a connection with Signal R and on further debugging the client code we see that the handshake request is getting failed.
Below is the client code:
Future<void> openChatConnection() async {
if (_hubConnection == null) {
//final logger = attachToLogger();
_hubConnection = HubConnectionBuilder().withUrl(_serverUrl).build();
_hubConnection.onclose((error) => connectionIsOpen = false);
_hubConnection.on("OnMessage", _handleIncommingChatMessage);
}
if (_hubConnection.state != HubConnectionState.Connected) {
await _hubConnection.start();
connectionIsOpen = true;
}
Below is the server code of application running in Service Fabric:
Startup.cs
public class Startup
{
public Startup(IHostingEnvironment env)
{
IConfigurationBuilder builder = new ConfigurationBuilder();
// .SetBasePath(env.ContentRootPath)
// // .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
// // .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
// .AddEnvironmentVariables();
this.Configuration = builder.Build();
}
public IConfigurationRoot Configuration { get; }
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
// Add framework services.
services.AddMvc();
services.AddSignalR();
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
// if (env.IsDevelopment())
// {
// app.UseDeveloperExceptionPage();
// app.UseBrowserLink();
// }
// else
// {
// app.UseExceptionHandler("/Home/Error");
// }
// app.UseStaticFiles();
app.UseSignalR(routes =>
{
routes.MapHub<NotificationClient>("/Chat");
});
app.UseMvc(
routes =>
{
routes.MapRoute(
name: "default",
template: "{controller=Home}/{action=Index}/{id?}");
});
app.UseCors();
}
}
}
Below is the Hub Class:
public class NotificationClient : Hub
{
#region Consts, Fields, Properties, Events
#endregion
#region Methods
public void Send(string name, string message)
{
// Call the "OnMessage" method to update clients.
Clients.All.SendCoreAsync("OnMessage", new object[]{name, message});
}
#endregion
}

IdentityServer4 don't shows permissions checkbox

after login in IdentityServer4 i can't see permisions checkbox and redirection after clicking allow button doesn't work.
So i can see Personal Information title and Application Access title and no checkbox to them.
I'm using asp.net core 2.0 and IdentityServer4
My configuration looks like that:
public class InMemoryConfiguration
{
public static IEnumerable<ApiResource> ApiResources()
{
return new[]
{
new ApiResource("checklist")
};
}
public static IEnumerable<IdentityResource> IdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Email()
};
}
public static IEnumerable<Client> Clients()
{
return new[]
{
new Client
{
ClientId = "checklist",
ClientSecrets = new [] {new Secret("secret".Sha256())},
AllowedGrantTypes = GrantTypes.ResourceOwnerPasswordAndClientCredentials,
AllowedScopes = new [] {
"checklist"
}
},
new Client
{
ClientId = "checklist_implicit",
ClientSecrets = new [] {new Secret("secret".Sha256())},
AllowedGrantTypes = GrantTypes.Implicit,
AllowAccessTokensViaBrowser = true,
AllowedScopes = new [] {
IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,
IdentityServerConstants.StandardScopes.Email,
"checklist"
},
RedirectUris = new [] {
"http://localhost:4200/callback"
},
PostLogoutRedirectUris = {"http://localhost:4200/home"},
//RequireConsent=false
}
};
}
public static IEnumerable<TestUser> Users()
{
return new[]
{
new TestUser
{
SubjectId = "1",
Username = "kuba#wp.pl",
Password = "password",
Claims = new [] {new Claim("email", "kuba#wp.pl")}
}
};
}
}
And startup.cs:
public void ConfigureServices(IServiceCollection services)
{
services.AddCors(options => {
// this defines a CORS policy called "default"
options.AddPolicy("default", policy => {
policy
.SetIsOriginAllowedToAllowWildcardSubdomains()
.WithOrigins("http://localhost:4200")
.AllowAnyHeader()
.AllowAnyMethod()
.AllowCredentials();
});
});
services.AddIdentityServer()
//.AddSigningCredential(new X509Certificate2(#"D:\kuba\git\checklist\certs\IdentityServer4Auth.pfx", "changeit"))
.AddDeveloperSigningCredential()
.AddTestUsers(InMemoryConfiguration.Users().ToList())
.AddInMemoryClients(InMemoryConfiguration.Clients())
.AddInMemoryIdentityResources(InMemoryConfiguration.IdentityResources())
.AddInMemoryApiResources(InMemoryConfiguration.ApiResources());
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, ILoggerFactory loggerFactory)
{
loggerFactory.AddConsole();
app.UseDeveloperExceptionPage();
app.UseCors("default");
app.UseIdentityServer();
app.UseStaticFiles();
app.UseMvcWithDefaultRoute();
}
On client side i'm using angular
No clue why, but in the Consent view - index.html,
Change this:
<partial name="_ScopeListItem" model="#scope" />
To:
#Html.Partial("_ScopeListItem", scope)
And the consent checkboxes show up as expected.

How to register AuthFeature from plugin?

I'm trying to register AuthProvider from plugin.
public class Plugin : IPlugin
{
public void Register(IAppHost appHost)
{
appHost.Plugins.Add(new AuthFeature(
() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(),
}
));
appHost.GetContainer().Register<ICacheClient>(new MemoryCacheClient());
var userRep = new InMemoryAuthRepository();
appHost.GetContainer().Register<IUserAuthRepository>(userRep);
}
}
Plugin is successfully loaded but when I open my endpoint in web browser I'm getting error:
The AuthService must be initialized by calling AuthService.Init to use an authenticate attribute
Plugins need to be registered before they're loaded which you can do in the constructor or by implementing the IPreInitPlugin interface to run custom logic before the plugins are loaded, e.g:
public class Plugin : IPlugin, IPreInitPlugin
{
public void Configure(IAppHost appHost)
{
appHost.Plugins.Add(new AuthFeature(
() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(),
}
));
}
public void Register(IAppHost appHost)
{
appHost.GetContainer().Register<ICacheClient>(new MemoryCacheClient());
var userRep = new InMemoryAuthRepository();
appHost.GetContainer().Register<IUserAuthRepository>(userRep);
}
}
An alternative API to load dependent plugins in the registration of a Plugin is to use the LoadPlugin API, e.g:
public class Plugin : IPlugin
{
public void Register(IAppHost appHost)
{
appHost.GetContainer().Register<ICacheClient>(new MemoryCacheClient());
var userRep = new InMemoryAuthRepository();
appHost.GetContainer().Register<IUserAuthRepository>(userRep);
appHost.LoadPlugin(new AuthFeature(
() => new AuthUserSession(),
new IAuthProvider[] {
new CredentialsAuthProvider(),
}
));
}
}

OWIN middleware gets called more times than expected, what am I missing?

I am testing OWIN middleware and wrote the following middleware class. But strangely, I see that this middleware is called about four times in total when I request a URL without any path (localhost:<port>). However when I call localhost:<port>/welcome.htm or when I call localhost:<port>/default.htm it is only called once as expected. What am I missing?
public class MyMiddleware : OwinMiddleware
{
public MyMiddleware(OwinMiddleware next)
: base(next)
{
}
public override async Task Invoke(IOwinContext context)
{
await this.Next.Invoke(context);
if (context.Request.Path.HasValue && (context.Request.Path.Value == "/" || context.Request.Path.Value.EndsWith(".htm")))
{
using (var writer = new StreamWriter(context.Response.Body))
{
await writer.WriteLineAsync("Next middleware: " + this.Next.ToString());
}
}
}
}
and here's my Startup configuration:
public void Configuration(IAppBuilder appBuilder)
{
appBuilder.UseErrorPage(new ErrorPageOptions {
ShowCookies = true,
ShowEnvironment = true,
ShowExceptionDetails = true,
ShowHeaders = true,
ShowQuery = true,
ShowSourceCode = true,
SourceCodeLineCount = 2
});
appBuilder.Use<MyMiddleware>();
appBuilder.UseWelcomePage("/welcome.htm");
appBuilder.UseStaticFiles();
}

Resources