Using ASP.NET MVC 5, I would like to return appropriate HTTP status code for different scenarios (401 for user is not authenticated, 403 when user has no right for some resource, etc.), then handle them in jQuery.
But the problem is, when I try to return 401, it always returns "200: OK". MVC 5 RC1 was giving "302: Found" instead of 401, so I could use a workaround (HttpStatusCodeResult(401) returns "302 Found").
But now I moved from MVC 5 RC1 to MVC 5 and this behaviour changed. Now it is always "200: OK". So my workaround is useless, of course I can't replace 200 with anything else.
public ActionResult My()
{
if (User.Identity.IsAuthenticated == false)
{
return new HttpStatusCodeResult(401, "User is not authenticated.");
// Returns "200: OK"
}
// ... other code ...
}
How to solve this?
The MVC 5+ Pipeline modifies 401 response codes.
Option 1 With .net 4.5
you can set HttpContext.Response.SuppressFormsAuthenticationRedirect to true.
e.g. in your custom AuthoriseAttribute.cs
protected override void HandleUnauthorizedRequest(AuthorizationContext filterContext)
{
if (filterContext.HttpContext.Request.IsAjaxRequest())
{
filterContext.Result = new JsonResult
{
Data = "_Logon_",
JsonRequestBehavior = JsonRequestBehavior.AllowGet
};
filterContext.HttpContext.Response.StatusCode = (int)HttpStatusCode.Unauthorized;
filterContext.HttpContext.Response.SuppressFormsAuthenticationRedirect = true;
}
Option 2. If not using .net 4.5
public class SuppressFormsAuthenticationRedirectModule : IHttpModule
{
private static readonly object SuppressAuthenticationKey = new object();
public static void Register()
{
DynamicModuleUtility.RegisterModule(
typeof(SuppressFormsAuthenticationRedirectModule));
}
public static void SuppressAuthenticationRedirect(HttpContext context)
{
context.Items[SuppressAuthenticationKey] = true;
}
public static void SuppressAuthenticationRedirect(HttpContextBase context)
{
context.Items[SuppressAuthenticationKey] = true;
}
public void Init(HttpApplication context)
{
context.PostReleaseRequestState += OnPostReleaseRequestState;
context.EndRequest += OnEndRequest;
}
public void Dispose()
{
}
private void OnPostReleaseRequestState(object source, EventArgs args)
{
var context = (HttpApplication)source;
var response = context.Response;
var request = context.Request;
if (response.StatusCode == 401 && request.Headers["X-Requested-With"] == "XMLHttpRequest")
{
SuppressAuthenticationRedirect(context.Context);
}
}
private void OnEndRequest(object source, EventArgs args)
{
var context = (HttpApplication)source;
var response = context.Response;
if (context.Context.Items.Contains(SuppressAuthenticationKey))
{
response.TrySkipIisCustomErrors = true;
response.ClearContent();
response.StatusCode = 401;
response.RedirectLocation = null;
}
}
}
and in web.config
<modules>
<add name="SuppressFormsAuthenticationRedirectModule" type="SuppressFormsAuthenticationRedirectModule"/>
</modules>
See here for more info
Solution to the issue can be found in http://kevin-junghans.blogspot.in/2013/12/returning-401-http-status-code-on.html
You need to modify your Startup class like this:
public partial class Startup
{
private static bool IsAjaxRequest(IOwinRequest request)
{
IReadableStringCollection query = request.Query;
if ((query != null) && (query["X-Requested-With"] == "XMLHttpRequest"))
{
return true;
}
IHeaderDictionary headers = request.Headers;
return ((headers != null) && (headers["X-Requested-With"] == "XMLHttpRequest"));
}
// For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
public void ConfigureAuth(IAppBuilder app)
{
// Enable the application to use a cookie to store information for the signed in user
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
Provider = new CookieAuthenticationProvider
{
OnApplyRedirect = ctx =>
{
if (!IsAjaxRequest(ctx.Request))
{
ctx.Response.Redirect(ctx.RedirectUri);
}
}
}
});
// Use a cookie to temporarily store information about a user logging in with a third party login provider
app.UseExternalSignInCookie(DefaultAuthenticationTypes.ExternalCookie);
}
}
I use simuler code that return 404. Your code could be:
public ActionResult My()
{
if (User.Identity.IsAuthenticated == false)
{
return new HttpUnauthorizedResult();
}
// ... other code ...
}
In Statup.cs I had to set AutomaticChallenge to false. Once I did that it stopped doing the URL redirect (which resulted in 200 status) and gave me the 401 status I desired.
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentity<ApplicationUser, ApplicationRole>(options =>
{
options.Cookies.ApplicationCookie.AutomaticChallenge = false; //<#######
//...
})
.AddEntityFrameworkStores<ApplicationDbContext, int>()
.AddDefaultTokenProviders();
}
If you have set some cookie middleware:
public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory, IAntiforgery antiforgery)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
AuthenticationScheme = "__auth__",
LoginPath = "/account/login",
AccessDeniedPath = "/account/forbidden",
AutomaticAuthenticate = true,
AutomaticChallenge = false //<#######
});
}
For Identity middleware, redirect can be disabled by removing in LoginPath option in Startup.Auth.cs:
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
...
LoginPath = new PathString("/Account/Login"), // Remove this line
});
Related
We have a Net Core 2.1 API project. We use the request headers to retrieve API key which we check against our database to see if it matches one of the expected keys. If it does then we allow the request to continue, otherwise we want to send back Unauthorized response.
our startup.cs
services.AddAuthorization(options =>
{
options.AddPolicy("APIKeyAuth", policyCorrectUser =>
{
policyCorrectUser.Requirements.Add(new APIKeyAuthReq());
});
});
services.AddSingleton<Microsoft.AspNetCore.Authorization.IAuthorizationHandler, APIKeyAuthHandler>();
Our APIKeyAuthHandler.cs
public class APIKeyAuthReq : IAuthorizationRequirement { }
public class APIKeyAuthHandler : AuthorizationHandler<APIKeyAuthReq>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, APIKeyAuthReq requirement)
{
if (context == null)
throw new ArgumentNullException(nameof(context));
if (requirement == null)
throw new ArgumentNullException(nameof(requirement));
var httpContext = context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext;
var headers = httpContext.HttpContext.Request.Headers;
if (headers.TryGetValue("Authorization", out Microsoft.Extensions.Primitives.StringValues value))
{
using (DBContext db = new DBContext ())
{
var token = value.First().Split(" ")[1];
var login = db.Login.FirstOrDefault(l => l.Apikey == token);
if (login == null)
{
context.Fail();
httpContext.HttpContext.Response.StatusCode = 403;
return Task.CompletedTask;
} else
{
httpContext.HttpContext.Items.Add("CurrentUser", login);
context.Succeed(requirement);
return Task.CompletedTask;
}
}
}
}
}
and our controller.cs
[Route("api/[controller]/[action]")]
[Authorize("APIKeyAuth")]
[ApiController]
public class SomeController : ControllerBase
{
}
Everything works fine when a valid key exists but when it doesnt, there is a 500 internal error thrown for No authenticationScheme instead of 403.
We are relatively new to net core (coming from Net Framework/Forms Authentication) so if there is more accurate way of doing this sort of auth, please let me know.
Error Message:
InvalidOperationException: No authenticationScheme was specified, and
there was no DefaultChallengeScheme found.
Microsoft.AspNetCore.Authentication.AuthenticationService.ChallengeAsync(HttpContext
context, string scheme, AuthenticationProperties properties)
Token based authentication is preferred. However, if you do need a custom ApiKeyAuth scheme, well, it's possible.
Firstly, it seems that Authorize("APIKeyAuth") does not make sense here, as we have to authenticate the user before authorization. When there's an incoming request, the server has no idea who the user is. So, let's move the ApiKeyAuth from Authorization to Authentication.
To do that, just create a dummy ApiKeyAuthOpts that can be used to hold options
public class ApiKeyAuthOpts : AuthenticationSchemeOptions
{
}
and a simple ApiKeyAuthHandler to handle authentication (I just copy some of your codes above):
public class ApiKeyAuthHandler : AuthenticationHandler<ApiKeyAuthOpts>
{
public ApiKeyAuthHandler(IOptionsMonitor<ApiKeyAuthOpts> options, ILoggerFactory logger, UrlEncoder encoder, ISystemClock clock)
: base(options, logger, encoder, clock)
{
}
private const string API_TOKEN_PREFIX = "api-key";
protected override async Task<AuthenticateResult> HandleAuthenticateAsync()
{
string token = null;
string authorization = Request.Headers["Authorization"];
if (string.IsNullOrEmpty(authorization)) {
return AuthenticateResult.NoResult();
}
if (authorization.StartsWith(API_TOKEN_PREFIX, StringComparison.OrdinalIgnoreCase)) {
token = authorization.Substring(API_TOKEN_PREFIX.Length).Trim();
}
if (string.IsNullOrEmpty(token)) {
return AuthenticateResult.NoResult();
}
// does the token match ?
bool res =false;
using (DBContext db = new DBContext()) {
var login = db.Login.FirstOrDefault(l => l.Apikey == token); // query db
res = login ==null ? false : true ;
}
if (!res) {
return AuthenticateResult.Fail($"token {API_TOKEN_PREFIX} not match");
}
else {
var id=new ClaimsIdentity(
new Claim[] { new Claim("Key", token) }, // not safe , just as an example , should custom claims on your own
Scheme.Name
);
ClaimsPrincipal principal=new ClaimsPrincipal( id);
var ticket = new AuthenticationTicket(principal, new AuthenticationProperties(), Scheme.Name);
return AuthenticateResult.Success(ticket);
}
}
}
At last, we still need a little of configuration to make them to work:
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);
services.AddAuthentication("ApiKeyAuth")
.AddScheme<ApiKeyAuthOpts,ApiKeyAuthHandler>("ApiKeyAuth","ApiKeyAuth",opts=>{ });
}
// 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.UseHttpsRedirection();
app.UseMvc();
}
When you send a request to action method protected by [Authorize]:
GET https://localhost:44366/api/values/1 HTTP/1.1
Authorization: api-key xxx_yyy_zzz
the response will be HTTP/1.1 200 OK. When you send a request without the correct key, the response will be:
HTTP/1.1 401 Unauthorized
Server: Kestrel
X-SourceFiles: =?UTF-8?B?RDpccmVwb3J0XDIwMThcOVw5LTEyXFNPLkFwaUtleUF1dGhcQXBwXEFwcFxhcGlcdmFsdWVzXDE=?=
X-Powered-By: ASP.NET
Date: Wed, 12 Sep 2018 08:33:23 GMT
Content-Length: 0
I'm new to OWIN and ADFS. I'm trying to authenticate users from ADFS using OWIN middleware. But when i run the app and perform login, the return HttpContext.Current.GetOwinContext() is not initialized properly.
owin_middleware_startup.cs
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, // application cookie which is generic for all the authentication types.
LoginPath = new PathString("/login.aspx"), // redirect if not authenticated.
AuthenticationMode = AuthenticationMode.Passive
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
MetadataAddress = "https://adfs-server/federationmetadata/2007-06/federationmetadata.xml", //adfs meta data.
Wtrealm = "https://localhost/", //reltying party
Wreply = "/home.aspx" // redirect
});
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
}
login.aspx.cs
private IAuthenticationManager AuthenticationManager
{
get { return HttpContext.Current.GetOwinContext().Authentication; }
}
protected void Page_Load(object sender, EventArgs e)
{
}
protected void loginSSObtn_Click(object sender, EventArgs e)
{
IdentitySignin("administrator");
}
private void IdentitySignin(string userName)
{
//Create list of claims for Identity
var claims = new List<Claim>();
claims.Add(new Claim(ClaimTypes.Name, userName));
var identity = new ClaimsIdentity(claims, DefaultAuthenticationTypes.ApplicationCookie);
AuthenticationManager.SignIn(new AuthenticationProperties()
{
AllowRefresh = true,
IsPersistent = true,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddDays(2)
}, identity);
//Response.Redirect("/home.aspx");
}
My goal is to redirect to the ADFS login and authenticate the user. Highly appreciate any help. Thanks.
Found the issue, I had missed the RUN method - app.Run() in the middle-ware. This inserts the extension to the OWIN startup. And executes it for all the requests.
Fix :
public void Configuration(IAppBuilder app)
{
// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=316888
ConfigureAuth(app);
}
public void ConfigureAuth(IAppBuilder app)
{
app.SetDefaultSignInAsAuthenticationType(DefaultAuthenticationTypes.ApplicationCookie);
app.UseCookieAuthentication(
new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie, // application cookie which is generic for all the authentication types.
LoginPath = new PathString("/login.aspx"), // redirect if not authenticated.
AuthenticationMode = AuthenticationMode.Passive
});
app.UseWsFederationAuthentication(
new WsFederationAuthenticationOptions
{
AuthenticationType = "test auth",
MetadataAddress = "https://adfs-server/federationmetadata/2007-06/federationmetadata.xml", //adfs meta data.
Wtrealm = "https://localhost/", //reltying party
Wreply = "/home.aspx"//redirect
});
AuthenticateAllRequests(app, "test auth");
}
private static void AuthenticateAllRequests(IAppBuilder app, params string[] authenticationTypes)
{
app.Use((context, continuation) =>
{
if (context.Authentication.User != null &&
context.Authentication.User.Identity != null &&
context.Authentication.User.Identity.IsAuthenticated)
{
return continuation();
}
else
{
context.Authentication.Challenge(authenticationTypes);
return Task.Delay(0);
}
});
}
But if we want to execute the extensions/middle-wares only for some specific path then we can use app.Use() this is just one usage of it.
feel free to correct me if i'm wrong.
Lets say I have a hub:
public class MyHub: Hub<IMyHub> {
public MyHub(){}
public Task DoWork(){
var principal = this.Context.User; // Currently WindowsIdentity as its not authenticated
var auth = new OwinContext(this.Context.Request.Environment).Authentication;
var types = auth.GetAuthenticationTypes(); // Empty list
// ....
}
}
If I execute same code inside an WebApi2 Controller the .GetAuthenticationTypes() would give me the correct result of pre'configured authProviders.
Any ideas why its not behaving like within an controller? Is that by design?
Update 1
Startup.cs
public class Startup
{
public void Configuration(IAppBuilder app)
{
var httpConfig = new HttpConfiguration();
// .. Ioc Registering Hubs
WebApiConfig.Register(httpConfig);
FilterConfig.RegisterGlobalFilters(GlobalFilters.Filters);
// Have also tried registering signalr here before auth
// app.MapSignalR("/signalR", new HubConfiguration() { .... });
app.UseOAuthIntrospection(options =>
{
//...
options.AuthenticationType = OAuthDefaults.AuthenticationType;
options.ClientId = clientId;
options.ClientSecret = clientSecret;
options.RequireHttpsMetadata = false;
options.AuthenticationMode = AuthenticationMode.Passive;
options.Events = new OAuthIntrospectionEvents()
{
OnRetrieveToken = context =>
{
// Getting token from QueryString passed from js app.
var token = context.Request.Query["Authorization"];
if (!string.IsNullOrWhiteSpace(token))
{
context.Token = token;
}
return Task.CompletedTask;
}
}
//...
};
app.UseCors(CorsOptions.AllowAll);
app.MapSignalR("/signalR", new HubConfiguration() { .... });
app.UseWebApi(httpConfig); // Have tried swapping these 0 effect
}
In your Owin startup part, you should configure app.MapSignalR(); before registering authentication.
public void Configuration(IAppBuilder app)
{
app.MapSignalR();//Configure it first
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Home/Index")
});
}
When I try to access Context.User in my Hub I keep getting Context.User is null errors. I have tried moving app.MapSignalR() under the ConfigureAuth() but that causes SignalR not to map at all. I'm not sure where I need to pass my cookie to SignalR at.
SignalR is working in my app for sending messages to all users I just can't get the OnConnect override to work without the Context.User
Startup.CS
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
ConfigureAuth(app);
}
}
Startup.Auth.cs
public partial class Startup
{
private static string clientId = ConfigurationManager.AppSettings["ida:ClientId"];
private static string appKey = ConfigurationManager.AppSettings["ida:ClientSecret"];
private static string aadInstance = ConfigurationManager.AppSettings["ida:AADInstance"];
private static string tenantId = ConfigurationManager.AppSettings["ida:TenantId"];
private static string postLogoutRedirectUri = ConfigurationManager.AppSettings["ida:PostLogoutRedirectUri"];
private static string authority = aadInstance + tenantId;
public void ConfigureAuth(IAppBuilder app)
{
app.MapWhen(context => !IsDataPath(context.Request), appBuilder =>
{
appBuilder.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions());
appBuilder.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
//AuthenticationMode = AuthenticationMode.Passive,
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
var claimsIdentity = context.AuthenticationTicket.Identity;
CleanupClaims(claimsIdentity);
AddHboClaims(claimsIdentity);
context.AuthenticationTicket.Properties.ExpiresUtc = DateTime.Now.AddDays(1).ToUniversalTime();
return Task.CompletedTask;
},
AuthenticationFailed = (context) =>
{
if (context.Exception.Message.StartsWith("OICE_20004") || context.Exception.Message.Contains("IDX10311"))
{
context.SkipToNextMiddleware();
return Task.FromResult(0);
}
return Task.FromResult(0);
},
}
});
});
}
private bool IsDataPath(Microsoft.Owin.IOwinRequest request)
{
return request.Path.Value.StartsWith("/data");
}
private void CleanupClaims(ClaimsIdentity claimsIdentity)
{
//Remove long unecessary claim types to make the cookie smaller
claimsIdentity.RemoveClaim(ClaimTypes.Surname);
claimsIdentity.RemoveClaim(ClaimTypes.GivenName);
claimsIdentity.RemoveClaim("onprem_sid");
claimsIdentity.RemoveClaim("http://schemas.microsoft.com/identity/claims/tenantid");
claimsIdentity.RemoveClaim("http://schemas.microsoft.com/claims/authnmethodsreferences");
claimsIdentity.RemoveClaim("ipaddr");
}
private void AddHboClaims(ClaimsIdentity claimsIdentity)
{
var depResolver = AutofacDependencyResolver.Current;
var permissionRespository = (IUserPermissionsRepository)depResolver.GetService(typeof(IUserPermissionsRepository));
var emailClaim = claimsIdentity.FindFirst(ClaimTypes.Upn);
var userPermissions = permissionRespository.GetPermissionForUser(emailClaim.Value);
foreach (var permission in userPermissions)
{
claimsIdentity.AddClaim(HboClaimsNames.Permission, ((int)permission).ToString());
}
var db = (Database.HboDbContext)depResolver.GetService(typeof(Database.HboDbContext));
var resource = db.Resources.SingleOrDefault(r => r.HmbEmail == emailClaim.Value);
if (resource != null)
{
//if (resource.IsActive)
//{
claimsIdentity.AddClaim(HboClaimsNames.ResourceId, resource.Id.ToString());
//}
//else
//{
// var ex = new Exception("Inactive user attempting to log into HBO: " + emailClaim.Value);
// Elmah.ErrorSignal.FromCurrentContext().Raise(ex);
//
//}
}
else
{
var ex = new Exception("User attempting to log into HBO that is not in Db: " + emailClaim.Value);
Elmah.ErrorSignal.FromCurrentContext().Raise(ex);
}
}
}
You need to change this:
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
ConfigureAuth(app);
}
To this:
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app);
}
And call MapSignalR() inside your ConfigureAuth()
The reason you can't just call MapSignalR() after ConfigureAuth() is due to this line:
app.MapWhen(context => !IsDataPath(context.Request), appBuilder =>
The source of the problem lies in the fact that the MapWhen() method branches the request pipeline and if you just call app.MapSignalR() in the Owin Startup you'll be initializing SignalR in the wrong request pipeline.
So your ConfigureAuth() method should look something like:
public void ConfigureAuth(IAppBuilder app)
{
app.MapWhen(context => !IsDataPath(context.Request), appBuilder =>
{
appBuilder.SetDefaultSignInAsAuthenticationType(CookieAuthenticationDefaults.AuthenticationType);
appBuilder.UseCookieAuthentication(new CookieAuthenticationOptions());
appBuilder.UseOpenIdConnectAuthentication(
new OpenIdConnectAuthenticationOptions
{
ClientId = clientId,
Authority = authority,
PostLogoutRedirectUri = postLogoutRedirectUri,
Notifications = new OpenIdConnectAuthenticationNotifications()
{
SecurityTokenValidated = (context) =>
{
...
},
AuthenticationFailed = (context) =>
{
...
},
}
});
appBuilder.MapSignalR();
});
}
I try to use owin authentication manager to authenticate users but User.Identity.IsAuthenticated is still false.
Startup.cs
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
app.MapSignalR();
}
}
Startup.Auth.cs
public partial class Startup
{
public static Func<UserManager<ApplicationUser>> UserManagerFactory { get; set; }
public Startup()
{
UserManagerFactory = () =>
{
var userManager = new UserManager<ApplicationUser>(new CustomUserStore());
return userManager;
};
}
public void ConfigureAuth(IAppBuilder app)
{
app.UseCookieAuthentication(new CookieAuthenticationOptions
{
AuthenticationType = DefaultAuthenticationTypes.ApplicationCookie,
LoginPath = new PathString("/Account/Login"),
LogoutPath = new PathString("/Account/LogOff"),
ExpireTimeSpan = TimeSpan.FromDays(7)
});
}
}
Some part of authentication action:
private async Task SignInAsync(ApplicationUser user, bool isPersistent)
{
var authManager = return HttpContext.GetOwinContext().Authentication;
authManager.SignOut(DefaultAuthenticationTypes.ApplicationCookie);
var identity = await userManager.CreateIdentityAsync(user, DefaultAuthenticationTypes.ApplicationCookie);
authManager.SignIn(new AuthenticationProperties { IsPersistent = isPersistent }, identity);
}
The identity creates successfully but SignIn method doesn't sign in a user. What's wrong?
It's a very stupid mistake. I have forgotten to call ConfigureAuth method.
public partial class Startup
{
public void Configuration(IAppBuilder app)
{
ConfigureAuth(app); // <-- this
app.MapSignalR();
}
}