AspNetCoreOData icon indicating copy to clipboard operation
AspNetCoreOData copied to clipboard

Querying Metadata Basic Auth Handler never called

Open vbray1979 opened this issue 3 years ago • 4 comments

I am enabling basic authentication and JwtBearer authentication mechanism. When querying metadata JwtBearer validation works but Basic Auth handler is never called. How to authenticate a user using basic authentication when querying metadata?

Remark: Querying controller with [Authorised] attribute trigger HandleAuthenticateAsync

Startup.cs

//Add Authentication
           services.AddAuthentication()
               .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, (o) =>
               {
                   o.TokenValidationParameters = new TokenValidationParameters()
                   {
                       IssuerSigningKey = TokenAuthOption.Key,
                       ValidAudience = TokenAuthOption.Audience,
                       ValidIssuer = TokenAuthOption.Issuer,
                       ValidateIssuerSigningKey = true,
                       ValidateLifetime = true,
                       ValidateIssuer = true,
                       ValidateAudience = true,
                       ClockSkew = TimeSpan.FromMinutes(0)
                   };
               })
               .AddScheme<AuthenticationSchemeOptions, BasicAuthenticationHandler>("Basic", null);

           //Add Authorization
           services.AddAuthorization((o) =>
           {
               // define Basic and Bearer default auth
               o.DefaultPolicy = new AuthorizationPolicyBuilder(new string[] { JwtBearerDefaults.AuthenticationScheme, "Basic" })
               .RequireAuthenticatedUser()
               .Build();

               //Only JwtBearer
               var onlyJwtBearerSchemePolicyBuilder = new AuthorizationPolicyBuilder(JwtBearerDefaults.AuthenticationScheme);
               o.AddPolicy(JwtBearerDefaults.AuthenticationScheme, onlyJwtBearerSchemePolicyBuilder
                   .RequireAuthenticatedUser()
                   .Build());

               //Only Basic
               var onlyBasicSchemePolicyBuilder = new AuthorizationPolicyBuilder("Basic");
               o.AddPolicy("Basic", onlyBasicSchemePolicyBuilder
                   .RequireAuthenticatedUser()
                   .Build());
           });

BasicAuthenticationHandler.cs

public class BasicAuthenticationHandler : AuthenticationHandler<AuthenticationSchemeOptions>
    {
        public BasicAuthenticationHandler(
           IOptionsMonitor<AuthenticationSchemeOptions> options,
           ILoggerFactory logger,
           UrlEncoder encoder,
           ISystemClock clock
           ) : base(options, logger, encoder, clock)
        {
        }

        protected override Task<AuthenticateResult> HandleAuthenticateAsync()
        {
            var authHeader = Request.Headers["Authorization"].ToString();
            if (authHeader != null && authHeader.StartsWith("basic", StringComparison.OrdinalIgnoreCase))
            {
                var token = authHeader.Substring("Basic ".Length).Trim();
                var credentialstring = Encoding.UTF8.GetString(Convert.FromBase64String(token));
                var credentials = credentialstring.Split(':');
                if (credentials[0] == "admin" && credentials[1] == "admin")
                {
                    //Build Claims
                    //var Claims = new List<Claim>();
                    //Claims.Add(new Claim(ClaimTypes.NameIdentifier, UserId.ToString()));
                    //Claims.Add(new Claim(ClaimTypes.Name, Username));
                    //Claims.Add(new Claim(ClaimTypes.Email, UserEmail));
                    //foreach (var userRole in UserRoles)
                    //    Claims.Add(new Claim(ClaimTypes.Role, userRole));

                    var claims = new[] { new Claim("name", credentials[0]), new Claim(ClaimTypes.Role, "Admin") };
                    var identity = new ClaimsIdentity(claims, "Basic");
                    var claimsPrincipal = new ClaimsPrincipal(identity);
                    return Task.FromResult(AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, Scheme.Name)));
                }

                Response.StatusCode = 401;
                Response.Headers.Add("WWW-Authenticate", "Basic realm=\"ami.com\"");
                return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
            }
            else
            {
                Response.StatusCode = 401;
                Response.Headers.Add("WWW-Authenticate", "Basic realm=\"ami.com\"");
                return Task.FromResult(AuthenticateResult.Fail("Invalid Authorization Header"));
            }
        }
    }

`

vbray1979 avatar Sep 21 '22 09:09 vbray1979

Can you try configuring your controllers with RequireAuthorization?

image

This should then set the default policy for each and every controller action, including the metadata controller.

Note that just configuring the DefaultPolicy object doesn't apply it unless you do that, or explicitly add an [Authorize] attribute on the controllers and actions themselves.

julealgon avatar Sep 21 '22 19:09 julealgon

This indeed enable the authorization on all controllers but it cannot be overriden using [AllowAnonymous] for the Login controller. Any suggestion to "RequireAuthorization" with AllowAnonymous enabled?

vbray1979 avatar Sep 23 '22 15:09 vbray1979

This indeed enable the authorization on all controllers but it cannot be overriden using [AllowAnonymous] for the Login controller. Any suggestion to "RequireAuthorization" with AllowAnonymous enabled?

Right.... if you need anonymous endpoints, it doesn't combine elegantly with the global authorization since it will still do authentication:

  • https://github.com/dotnet/aspnetcore/issues/29377

I wonder here if the proper solution would be to just configure a policy in the metadatacontroller which is "anonymous by default" but that can then be configured by the consumer.

I don't see a decent way of configuring authorization on the metadata controller otherwise...

I guess you could just inherit from OData's metadata controller and then add the attribute to it, but I really despise using inheritance for this purpose...

The controller is here though:

  • https://github.com/OData/AspNetCoreOData/blob/69eec03c7003fe12d92cdc619efdc16781683694/src/Microsoft.AspNetCore.OData/Routing/Controllers/MetadataController.cs#L20

I gave another approach a shot over here.... not a big fan of it, but it seems to work.

You can create a controller convention to alter the metadata controller this way:

public sealed class MetadataControllerAuthorizationConvention : IControllerModelConvention
{
    void IControllerModelConvention.Apply(ControllerModel controller)
    {
        if (controller.ControllerType == typeof(MetadataController).GetTypeInfo())
        {
            controller.Filters.Add(new AuthorizeFilter(policy: default(string)!));
        }
    }
}

Notice I'm passing null as the policy there: that will result in the default policy being used.

Then, you can register the convention like this:

.AddControllers(c => c.Conventions.Add(new MetadataControllerAuthorizationConvention()))

This should be enough to fire the default auth policy whenever you access the metadata endpoint.

Don't quote me on this being the best way to handle this however.... it feels to me like there should be a simpler way here, but alas...

julealgon avatar Sep 23 '22 19:09 julealgon