in my .net 5 website i have to read user login from header and the call external webservice to check if is authorized and get permission list.
EDIT 3:
GOALS
- Read current user from http header setted by corporate single sign-on
- Read user permission and info by calling external web services and keep them daved to prevent extra-calls for every action
- let the user be free to access by any page
- authorize by default all controller's actions with custom claims
Actual Problem
context.User.Identity.IsAuthenticated in middleware is always false
Actual code
Startup - ConfigureServices
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie();
services.AddControllers(options => { options.Filters.Add<AuditAuthorizationFilter>(); });
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
Startup - Configure
app.UseMiddleware<AuthenticationMiddleware>();
app.UseAuthentication();
app.UseAuthorization();
Middleware
public class AuthenticationMiddleware
{
private readonly RequestDelegate _next;
// Dependency Injection
public AuthenticationMiddleware(RequestDelegate next)
{
_next = next;
}
public async Task Invoke(HttpContext context)
{
if (!context.User.Identity.IsAuthenticated)
{
var claims = new List<Claim>
{
new Claim(ClaimTypes.Name, context.Request.Headers["Token"]),
};
var claimsIdentity = new ClaimsIdentity(claims, CookieAuthenticationDefaultsAuthenticationScheme);
var authProperties = new AuthenticationProperties();
await context.SignInAsync(
CookieAuthenticationDefaults.AuthenticationScheme,
new ClaimsPrincipal(claimsIdentity),
authProperties);
}
await _next(context);
}
}
Filter
public class AuditAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
public int Order => -1;
private readonly IHttpContextAccessor _httpContextAccessor;
public AuditAuthorizationFilter(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}
public void OnAuthorization(AuthorizationFilterContext context)
{
if (context.HttpContext.User.Identity.IsAuthenticated)
{
context.Result = new ForbidResult();
}
else
{
string metodo = $"{context.RouteData.Values["controller"]}/{context.RouteData.Values["action"]}";
if (!context.HttpContext.User.HasClaim("type", metodo))
{
context.Result = new ForbidResult();
}
}
}
}
EDIT 2:
my Startup
public void ConfigureServices(IServiceCollection services)
{
services.AddDevExpressControls();
services.AddTransient<ILoggingService, LoggingService>();
services.AddHttpContextAccessor();
services.AddMvc().SetCompatibilityVersion(Microsoft.AspNetCore.Mvc.CompatibilityVersion.Version_3_0);
services.ConfigureReportingServices(configurator => {
configurator.UseAsyncEngine();
configurator.ConfigureWebDocumentViewer(viewerConfigurator => {
viewerConfigurator.UseCachedReportSourceBuilder();
});
});
services.AddControllersWithViews().AddJsonOptions(options => options.JsonSerializerOptions.PropertyNamingPolicy = null);
services.AddControllersWithViews().AddRazorRuntimeCompilation();
services.AddControllers(options => { options.Filters.Add(new MyAuthenticationAttribute ()); });
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.IdleTimeout = TimeSpan.FromSeconds(10);
options.Cookie.HttpOnly = true;
options.Cookie.IsEssential = true;
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env, ILoggerFactory loggerFactory)
{
app.UseDevExpressControls();
app.UseExceptionHandlerMiddleware(Log.Logger, errorPagePath: "/Error/HandleError" , respondWithJsonErrorDetails: true);
app.UseStatusCodePagesWithReExecute("/Error/HandleError/{0}");
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseSerilogRequestLogging(opts => opts.EnrichDiagnosticContext = LogHelper.EnrichFromRequest);
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseSession();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}
EDIT 1: to adapt original code to .net 5 i made some changes:
if (!context.HttpContext.User.Identity.IsAuthenticated)
{
const string MyHeaderToken = "HTTP_KEY";
string userSSO = null;
if (string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
{
userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
}
if (string.IsNullOrWhiteSpace(userSSO))
{
//filterContext.Result = new unh();
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, "My");
//string[] methods = new string[0]; // GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, null);
IdentityUser user = new (userSSO);
Thread.CurrentPrincipal = principal;
}
}
but context.HttpContext.User.Identity.IsAuthenticated is false everytimes, even if the previous action set principal
ORIGINAL:
I'using custom attribute to manage this scenario in this way:
public class MyAuthenticationAttribute : ActionFilterAttribute, IAuthenticationFilter{
public string[] Roles { get; set; }
public void OnAuthentication(AuthenticationContext filterContext)
{
string MyHeaderToken = “SM_USER”;
string userSSO = null;
if (HttpContext.Current.Request.Headers[MyHeaderToken] != null)
{
userSSO = HttpContext.Current.Request.Headers[MyHeaderToken];
Trace.WriteLine(string.Format(“got MyToken: {0}”, userSSO));
}
if (string.IsNullOrWhiteSpace(userSSO))
{
Trace.WriteLine(“access denied, no token found”);
}
else
{
// Create GenericPrincipal
GenericIdentity webIdentity = new GenericIdentity(userSSO, “My”);
string[] methods= GetMethods(userSSO);
GenericPrincipal principal = new GenericPrincipal(webIdentity, methods);
filterContext.HttpContext.User = principal;
}
}
public void OnAuthenticationChallenge(AuthenticationChallengeContext filterContext)
{
//check authorizations
}
}
but external webservice returns list of controller/action authorized for users, so i have to test all actions executions to simply check if names is contained in the list.
is there a way to do this without have to write attribute on every actions or every controllers in this way:
[MyAuthentication(Roles = “Admin”)]
pubic class AdminController: Controller
{
}
i know i can use
services.AddMvc(o =>
{
var policy = new AuthorizationPolicyBuilder()
.RequireAuthenticatedUser()
.Build();
o.Filters.Add(new AuthorizeFilter(policy));
});
but no idea of how to use this with my custom authorization
i'am also not sure if string[] methods= GetMethods(userSSO)
is cached by .net core filterContext.HttpContext.User
avoiding multiple calls to external webservice.
Thanks