12

I am developing an intranet asp.net core web api application. The requirements for authentications are:

  • REQ1 - when user which is trying to access the website is not in Active Directory's special group (let's name it "commonUsers") it is simply not authorized
  • REQ2 - when user which is trying to access the website is in Active Directory's group "commonUsers" is is authorized and a web resource is returned
  • REQ3 - when user which is trying to access the website is in Active Directory's group "superUser", it need to be prompted for his domain password once again (because it tries to access some very restricted resources)

Now, what I have so far:

  • My service is hosted using http.sys server in order to support windows authentication.
  • I am using claims transformer middlewere in order to check the user's Active Directory group, let's say something like this:

    public class ClaimsTransformer : IClaimsTransformation {
    private readonly IAuthorizationService _authorizationService;
    public ClaimsTransformer(IAuthorizationService authorizationService)
    {
        _authorizationService = authorizationService;
    }
    
    public Task<ClaimsPrincipal> TransformAsync(ClaimsPrincipal principal)
    {
        _authorizationService.Authorize(principal as  IHmiClaimsPrincipal);
        return Task.FromResult(principal);
    }}
    
  • I have specified a special policies also in my service configuration, for instance something like that:

    services.AddAuthorization(options =>
        {
            options.AddPolicy("TestPolicy", policy => 
                                       policy.RequireClaim(ClaimTypes.Role, "TestUser"));
            options.AddPolicy("TestPolicy2", policy => 
                                       policy.RequireClaim(ClaimTypes.Role, "SuperUser"));
        });
  • I am using [Authorize] attribute with specific policy in order to restrict access to specific resources based on policies

Now the question is, how should I satisfy REQ3?

Mike
  • 850
  • 10
  • 33
niao
  • 4,972
  • 19
  • 66
  • 114

3 Answers3

2

I think I would try to use MVC Filters : https://learn.microsoft.com/en-us/aspnet/core/mvc/controllers/filters?view=aspnetcore-2.2#authorization-filters

Filters run after all Middleware, but before the Action. This will allow you to control the redirect to credentials page just for specific actions or controllers. Whilst normally this is not the recommended method for authorization, I think it fits your requirements for a hybrid secondary authentication.

public class SuperUserFilter : Attribute, IAuthorizationFilter
{
    public void OnAuthorization(AuthorizationFilterContext context)
    {
        if (context.HttpContext.Request.Cookies.TryGetValue("SuperUserCookie", out string cookieVal))
        {
            if (!IsValidCookie(cookieVal))
                context.Result = LoginPage(context);

        }
        else
        {
            context.Result = LoginPage(context);
        }
    }

    private bool IsValidCookie(string cookieVal)
    {
        //validate cookie value somehow
        // crytpographic hash, store value in session, whatever
        return true;
    }

    private ActionResult LoginPage(AuthorizationFilterContext context)
    {
        return new RedirectToActionResult("SuperUser", "Login",
            new {redirectUrl = context.HttpContext.Request.GetEncodedUrl()});
    }
}

Then you create a Login Controller

public class LoginController : Controller
{    
    [HttpGet]    
    public IActionResult SuperUser(string redirectUrl)
    {
        // return a page to enter credentials
        // Include redirectUrl as field
    }

    [HttpPost]
    public IActionResult SuperUser(LoginData loginData)
    {
        // Validate User & Password
        Response.Cookies.Append("SuperUserCookie", "SomeValue");
        return Redirect(loginData.RedirectUrl);
    }
}

Then you can decorate specific actions (or controllers) as required:

public class MyController : Controller
{
    [HttpGet]
    [SuperUserFilter]
    public IActionResult MySensitiveAction()
    {
        // Do something sensitive
    }
}
ste-fu
  • 6,879
  • 3
  • 27
  • 46
0

I'm guessing you are try to implement two step authentication for some of your resource.
To do that you must use multiple authentication scheme and Authorize policies, but it's difficult because windows authentication is not controllable. we need to use some trick to know this is your second login.

authentication

  1. The Default Authenticaiton Scheme : Windows, it's the basic scheme for authenticate a windows user.
  2. Second Cookies base Authentication scheme : SuperUserTwoStep. we need this to goto our custom login logic.

Authorize

  1. the Authorize policies for specified scheme.
  2. a login page for login to SuperUserTwoStep scheme.
//startup
            services.AddAuthentication(HttpSysDefaults.AuthenticationScheme)
                .AddCookie("SuperUserTwoStep",op=>op.LoginPath = "/account/superuser2steplogin");

            services.AddAuthorization(op =>
            {
                op.AddPolicy("SuperUser", b => b.AddAuthenticationSchemes("SuperUserTwoStep")
                    .RequireAuthenticatedUser()
                    .RequireClaim(ClaimTypes.Role, "SuperUser"));
            });

// login 
        public static IDictionary<string, string> States { get; set; } = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);

        [Route("/account/superuser2steplogin")]
        public async Task<IActionResult> LoginTwoStepConfirm(string returnUrl, [FromServices]IAuthorizationService authorizationService,
            [FromServices]IAuthorizationPolicyProvider policyProvider)
        {

            var winresult = await HttpContext.AuthenticateAsync(IISDefaults.AuthenticationScheme);
            if (winresult.Succeeded)
            {
                if (States.TryGetValue(winresult.Principal.Identity.Name, out _))
                {
                    States.Remove(winresult.Principal.Identity.Name);
                    var principal = new System.Security.Claims.ClaimsPrincipal(new System.Security.Claims.ClaimsIdentity(winresult.Principal.Claims,"twostepcookie"));
                    await HttpContext.SignInAsync("SuperUserTwoStep", principal);
                    return Redirect(returnUrl);
                }
                else
                {
                    States[winresult.Principal.Identity.Name] = "1";
                    return Challenge(IISDefaults.AuthenticationScheme);
                }
            }

            else
            {
                return Challenge(IISDefaults.AuthenticationScheme);
            }
        }

        [Authorize("SuperUser")]
        public IActionResult YourSecurePage()
        {
            return Content("hello world");
        }

the most difficult thing is to track that this is the second time to login, I try to use cookie , but it doen't work, so I crate a static IDitionary<string,string> to track ,maybe use distributed cache is better

John
  • 716
  • 1
  • 7
  • 24
  • I will check that. – niao May 17 '19 at 14:21
  • This seems to work, however what is a situation when I want to specific resource, for instance /api/TestController/SomeAction to be accessible by SuperUser which should be prompted for credentials again (in this case PUT and GET operations are agailable) and other type of user (which should not be prompted for credentials again) - in this case read-only resources are available (GET actions)? – niao May 24 '19 at 07:48
  • @niao in this case you can define a [`IAuthorizeRequirement`](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.2#requirements) and a `handler` to handle the authorze policy yourself. ps: also u can try use `xhr` to access a "twostep" api, not sure it will show the credentials Popup or not . – John May 25 '19 at 07:05
  • @niao also consider for `@stefu` 's suggestion, use other authentication method to confirm the two step authorization (for example use the confirm code send by sms or email or even show on the screen), windows login is just too hard to control – John May 25 '19 at 07:15
0

I think in my opinion you should consider using: Policy-based authorization with Requirements, basically you have different authorization requirements that you want to treat them on and AND basis

REQ1 and REQ2 and REQ3

Here you have the link to the documentation: Requirements

But you need to understand that identity != permissions, the guys that introduce this concept of policies to Microsoft created a project named: PolicyServer and it is opensource: PolicyServer Git and they created a pattern there of how you should use your policies. Basically, you have external and internal users that are authenticated against your AD, all internal users should have permissions assigned to a role. And you only decorate your controller action with the permission rule you created for that policy

[Authorize("PerformSurgery")]
public async Task<IActionResult> PerformSurgery()
{
    // omitted
}

To understand the code and how they evaluate a policy, I think you should see the video they have online on the website: Policy Server

Hope this helps

Zinov
  • 3,817
  • 5
  • 36
  • 70