-1

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

gt.guybrush
  • 1,320
  • 3
  • 19
  • 48
  • 3
    you can have a look to this article [A better way to handle authorization in ASP.NET Core](https://www.thereformedprogrammer.net/a-better-way-to-handle-authorization-in-asp-net-core/) I think it can help you. – B.S. Jan 23 '21 at 19:20
  • great article, found stuff i will use but still i'missing how to avoid decorating all controller since i don't have any role to check but simply i want to test action name against user allowed actions list – gt.guybrush Jan 24 '21 at 09:38
  • maybe will be sore simple to save user method list in session and check in it every time a controller action is invoked, but don't know if there are better solution. sugegster link is over complicated since it use user login that i don.t have: user never logins since application is under single sign on – gt.guybrush Mar 23 '21 at 14:25

1 Answers1

1

If you want to apply your custom IAuthenticationFilter globally then you can do the ff:

services.AddControllers(options =>
{
    options.Filters.Add(new MyAuthenticationFilter());
});

With this approach, you no longer need to inherit from ActionFilterAttribute and no need to add the [MyAuthentication(Roles = “Admin”)] attributes.

Just ensure that you are allowing anonymous requests to actions that doesn't need authentication and/or authorization.

EDIT 2:

For your updated setup, make sure you do the ff:

  • Add cookie authentication

services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) .AddCookie();

  • Order of middlewares

    • app.UseRouting();
    • app.UseAuthentication();
    • app.UseMiddleware<AuthenticationMiddleware>();
    • app.UseAuthorization();

EDIT 1:

i'am also not sure if string[] methods= GetMethods(userSSO) is cached by .net core filterContext.HttpContext.User avoiding multiple calls to external webservice.

The lifetime of the filter depends on how you implemented it, usually it is singleton but you can make it transient by following the approach below:

public class MyAuthorizationFilter : IAuthorizationFilter, IOrderedFilter
{
    public int Order => -1; // Ensures that it runs first before basic Authorize filter

    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (!context.HttpContext.User.Identity.IsAuthenticated)
        {
            if (context.HttpContext.Session.IsAvailable 
                && context.HttpContext.Session.TryGetValue("_SessionUser", out byte[] _user))
            {
                SessionUser su = (SessionUser)this.ByteArrayToObject(_user);
                GenericPrincipal principal = this.CreateGenericPrincipal(su.IdentityName, su.Type, su.Roles);
                context.HttpContext.User = principal;
            }
            else
            {
                const string MyHeaderToken = "HTTP_KEY";

                string userSSO = null;
                if (!string.IsNullOrWhiteSpace(context.HttpContext.Request.Headers[MyHeaderToken]))
                {
                    userSSO = context.HttpContext.Request.Headers[MyHeaderToken];
                }
                userSSO = "TestUser";
                if (string.IsNullOrWhiteSpace(userSSO))
                {
                    //filterContext.Result = new unh();
                }
                else
                {
                    string identityType = "My";
                    string[] methods = new string[0]; // GetMethods(userSSO);
                    // Create GenericPrincipal
                    GenericPrincipal principal = this.CreateGenericPrincipal(userSSO, identityType, methods);
                    context.HttpContext.User = principal;
                    
                    if (context.HttpContext.Session.IsAvailable)
                    {
                        SessionUser su = new SessionUser()
                        {
                            IdentityName = principal.Identity.Name,
                            Type = principal.Identity.AuthenticationType,
                            Roles = methods
                        };

                        byte[] _sessionUser = this.ObjectToByteArray(su);
                        context.HttpContext.Session.Set("_SessionUser", _sessionUser);
                    }
                }
            }                
        }
    }

    private GenericPrincipal CreateGenericPrincipal(string name, string type, string[] roles)
    {
        GenericIdentity webIdentity = new GenericIdentity(name, type);
        GenericPrincipal principal = new GenericPrincipal(webIdentity, roles);

        return principal;
    }

    // Convert an object to a byte array
    private byte[] ObjectToByteArray(Object obj)
    {
        BinaryFormatter bf = new BinaryFormatter();
        using (var ms = new MemoryStream())
        {
            bf.Serialize(ms, obj);
            return ms.ToArray();
        }
    }

    // Convert a byte array to an Object
    private Object ByteArrayToObject(byte[] arrBytes)
    {
        using (var memStream = new MemoryStream())
        {
            var binForm = new BinaryFormatter();
            memStream.Write(arrBytes, 0, arrBytes.Length);
            memStream.Seek(0, SeekOrigin.Begin);
            var obj = binForm.Deserialize(memStream);
            return obj;
        }
    }

    [Serializable]
    private class SessionUser
    {
        public string IdentityName { get; set; }
        public string Type { get; set; }
        public string[] Roles { get; set; }
    }
}

public class MyAuthorizationAttribute : TypeFilterAttribute
{
    public MyAuthorizationAttribute()
        : base(typeof(MyAuthorizationFilter))
    {
    }
}

On Startup.cs > Configure call app.UseSession(); immediately after app.UseRouting() so that session will be available during authorization.

The code above will set the current HTTP Context's user and save it on session. Subsequent requests will attempt to use the user stored on the session. This will also make the DI container manage the lifetime of the filter. Read more about it in Filters in ASP.NET Core.

I do not recommend you follow this approach. Please do either cookie or token-based authentication by taking advantage of the authentication middleware in .NET Core.

Once the request reaches the action execution, context.HttpContext.User.Identity.IsAuthenticated will now be true.

jegtugado
  • 5,081
  • 1
  • 12
  • 35
  • done, but now iam facing problem in making user persistent after first action call. i add an edit to question – gt.guybrush Mar 24 '21 at 13:26
  • @gt.guybrush are you calling `.UseAuthentication()` on Configure after `.UseRouting()`? – jegtugado Mar 24 '21 at 14:09
  • startup detail added – gt.guybrush Mar 24 '21 at 14:30
  • 1
    I've updated my answer since I had free time to test it out. – jegtugado Mar 24 '21 at 17:12
  • edited as you suggested but Identity still lose all previous settings – gt.guybrush Mar 25 '21 at 15:08
  • @gt.guybrush what do you mean by losing all previous settings? Have you confirmed that it hits `context.HttpContext.User = principal;`? Do you have any other authorization filters? I tested the above code and the assigned principal persists in the action's `User.Identity`. – jegtugado Mar 26 '21 at 09:25
  • yes user is setting on first action, but in the next action called by user it's empty. to be clear: i need to set user (and other stuff read from external webservices) first time users open website and keep them until users logout – gt.guybrush Mar 26 '21 at 12:58
  • Authentication is made with cookies or tokens. You have neither, so next request by the same user will need to pass the conditions on your authorization filter so that the filter will set the principal again. If you use cookies or tokens then it will persist. – jegtugado Mar 26 '21 at 14:40
  • yes, i got it tooday: i'am implementing this: https://learn.microsoft.com/it-it/aspnet/core/security/authentication/cookie?view=aspnetcore-5.0 – gt.guybrush Mar 26 '21 at 15:00
  • @gt.guybrush It is better to follow that as it adheres to proper authentication. I've also updated my answer to satisfy your problem in an approach I do not recommend. – jegtugado Mar 26 '21 at 15:18
  • i still miss a things to implement cookie auth mixed with AuthFilter: how to get access to HttpContext.SignInAsync since don't have any login page and user can directly access to every page of my site – gt.guybrush Mar 26 '21 at 15:36
  • @gt.guybrush if you are using web api's then it is better to go token-based authentication and only use cookies if you are developing an mvc/blazor/razor web app. Cookies are limited to a single domain which is why you may consider tokens. Weight your options as to what you really need. Ask yourself, who will authenticate your users? External apis is most likely token-based. – jegtugado Mar 29 '21 at 08:11
  • it's a mvc web app. i'am mixing https://stackoverflow.com/questions/46938511/asp-net-core-2-0-custom-middleware-using-cookie-authentication and https://andrewlock.net/exploring-the-cookieauthenticationmiddleware-in-asp-net-core/ but still dont' find how to put all together. i'am adding actual status in question – gt.guybrush Mar 29 '21 at 08:49
  • @gt.guybrush I have updated my answer. I tested your authentication middleware and it looks good. – jegtugado Mar 29 '21 at 11:26
  • yeah, app.UseAuthentication(); before app.UseMiddleware(); made it working – gt.guybrush Mar 29 '21 at 14:39