Fake Developer
Fake Developer

Reputation: 25

Adding Bearer Token to ASP.NET Web API that has Basic Authentication Attribute

I have an ASP.Net Web API 2 with BasicAuthenticationAttribute that is working as expected. In my application, there are different controllers and I want to add bearer token-based authentication to one of my controllers. I added those NuGet packages:

Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
Microsoft.Owin.Security.OAuth

Here is the WebApiConfig:

public static class WebApiConfig
    {
        public static void Register(HttpConfiguration config)
        {
            

            config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
            config.Formatters.JsonFormatter.SerializerSettings.DateTimeZoneHandling =
                DateTimeZoneHandling.Local;
            
            config.MapHttpAttributeRoutes();

            config.Routes.MapHttpRoute(
                "DefaultApi",
                "api/{controller}/{id}",
                new {id = RouteParameter.Optional}
            );

            
            config.MessageHandlers.Add(new RequestResponseHandler());
            
            config.Filters.Add(new CustomExceptionFilter());
                       
            var resolver = config.DependencyResolver; //Assuming one is set.
            var basicAuth = (BasicAuthenticationAttribute)resolver.GetService(typeof(BasicAuthenticationAttribute));
            // Web API configuration and services
            if (basicAuth != null) config.Filters.Add(basicAuth);
           
        }
    }

Here is the Owin Startup

public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

            var configuration = GlobalConfiguration.Configuration;

            WebApiConfig.Register(configuration);
            app.UseWebApi(configuration);

            Configure(app);
        }

        private static void Configure(IAppBuilder app)
        {
            var options = new OAuthAuthorizationServerOptions()
            {
                TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
                AllowInsecureHttp = true,
                Provider = new AuthorizationServerProvider()
            };

            app.UseOAuthAuthorizationServer(options); 
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }
    }

Here is the controller

[RoutePrefix("api/v2/game/abc101")]
    public class A101Controller : ApiController
    {
        private readonly IGameServicesABC101 _gameServices;
        private readonly IMapper _mapper;

        public A101Controller(IGameServicesABC101 gameServices, IMapper mapper)
        {
            _gameServices = gameServices;
            _mapper = mapper;
        }

        [HttpPost]
        [Authorize]
        [Route("purchase")]
        public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
        {
            if (!ModelState.IsValid) return BadRequest(ModelState);
...

Basic Authentication Attribute

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
    {
        private const string Realm = "My Realm";
        private readonly Func<IUserValidate> _factory;

        public BasicAuthenticationAttribute(Func<IUserValidate> factory)
        {
            _factory = factory;
        }


        public override void OnAuthorization(HttpActionContext actionContext)
        {
            
            if (actionContext.Request.Headers.Authorization == null)
            {
                actionContext.Response = actionContext.Request
                    .CreateResponse(HttpStatusCode.Unauthorized);
               
                if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
                    actionContext.Response.Headers.Add("WWW-Authenticate",
                        $"Basic realm=\"{Realm}\"");
            }
            else
            {
               
                var authenticationToken = actionContext.Request.Headers
                    .Authorization.Parameter;
                try
                {
                    //Decode the string
                    var decodedAuthenticationToken = Encoding.UTF8.GetString(
                        Convert.FromBase64String(authenticationToken));
                   
                    var usernamePasswordArray = decodedAuthenticationToken.Split(':');
                    
                    var username = usernamePasswordArray[0];
                    
                    var password = usernamePasswordArray[1];
                    
                    var uv = _factory();
                    if (uv.Login(username, password))
                    {
                        var identity = new GenericIdentity(username);
                        IPrincipal principal = new GenericPrincipal(identity, null);
                        Thread.CurrentPrincipal = principal;
                        if (HttpContext.Current != null) HttpContext.Current.User = principal;
                    }
                    else
                    {
                        actionContext.Response = actionContext.Request
                            .CreateResponse(HttpStatusCode.Unauthorized);
                    }
                }
                catch
                {
                    actionContext.Response = actionContext.Request
                        .CreateResponse(HttpStatusCode.Unauthorized);
                }
            }
        }
    }

I am using Unity in my application. Basic Authentication works as expected. When I make a request without a token to ...api/v2/game/abc101/purchase I get a response either. Shouldn't I get 401? What I am missing?

UPDATE

I am searching and trying to find how to use both basic authentication and token-based authentication for different controllers. Here is my status update.

There is no code in the Global.asax

Here is the Owin Startup

public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

            var config = GlobalConfiguration.Configuration;
            WebApiConfig.Register(config);
            app.UseWebApi(config);
            Configure(app, config.DependencyResolver);           
            config.EnsureInitialized();
        }

        private static void Configure(IAppBuilder app, IDependencyResolver resolver)
        {
            var options = new OAuthAuthorizationServerOptions()
            {

                TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
                AllowInsecureHttp = true,
                Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
                
            };

            
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
            app.UseOAuthAuthorizationServer(options);

        }
    }

Here is AuthorizationServerProvider

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        private readonly IUserValidate _userValidate;
        public AuthorizationServerProvider(IUserValidate userValidate)
        {
            _userValidate = userValidate;
        }
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
            {
                context.SetError("Error", "Error...");
            }

            if (_userValidate.Login(clientId, clientSecret))
            {
                context.Validated();
            }
        }

        
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {

            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Origin", new[] { "*" });
            context.OwinContext.Response.Headers.Add("Access-Control-Allow-Headers", new[] { "Content-Type" });


            if (_userValidate.Login(context.UserName, context.Password))
            {
                
                var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                identity.AddClaim(new Claim("sub", context.UserName));
                identity.AddClaim(new Claim("role", "admin"));

                context.Validated(identity);
            }
            else
            {
                
                context.SetError("Error", "Error...");
            }
        }
    }

The rest is the same as the previous code samples.

When I call ...api/v2/game/abc101/purchase I am getting 401, it is progress. But when I call http://localhost:52908/token I am getting unsupported_grant_type. I am sending requests via Postman and I am sending a POST requests with content-type x-www-form-urlencoded. Grant-Type is password and username/password is also correct. When I call another controller http://localhost:52908/api/v2/game/purchase basic authentication does NOT work!

Hope someone can help.


UPDATE 1

Now I am getting the token, one step at a time :) How can I also use Basic authentication for another controller?

Here is Startup

public class Startup
    {
        public void Configuration(IAppBuilder app)
        {

            var config = GlobalConfiguration.Configuration;
            Configure(app, config.DependencyResolver);
            WebApiConfig.Register(config);
            app.UseCors(Microsoft.Owin.Cors.CorsOptions.AllowAll);
            app.UseWebApi(config);
            config.EnsureInitialized();

        }

        private static void Configure(IAppBuilder app, IDependencyResolver resolver)
        {
            var options = new OAuthAuthorizationServerOptions()
            {
                AllowInsecureHttp = true,
                TokenEndpointPath = new Microsoft.Owin.PathString("/token"),
                AccessTokenExpireTimeSpan = TimeSpan.FromHours(1),
                Provider = new AuthorizationServerProvider((IUserValidate)resolver.GetService(typeof(IUserValidate)))
                
            };

            app.UseOAuthAuthorizationServer(options);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());

        }
    }

Here is the Authorization Server Provider

public class AuthorizationServerProvider : OAuthAuthorizationServerProvider
    {
        private readonly IUserValidate _userValidate;
        public AuthorizationServerProvider(IUserValidate userValidate)
        {
            _userValidate = userValidate;
        }
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            if (!context.TryGetBasicCredentials(out var clientId, out var clientSecret))
            {
                context.SetError("invalid_grant", "The user name or password is incorrect.");
            }

            if (_userValidate.Login(clientId, clientSecret))
            {
                context.Validated();
            }
        }

        
        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {

            
            if (_userValidate.Login(context.UserName, context.Password))
            {
                
                var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                identity.AddClaim(new Claim("sub", context.UserName));
                identity.AddClaim(new Claim("role", "admin"));

                context.Validated(identity);
            }
            else
            {
                
                context.SetError("invalid_grant", "The user name or password is incorrect.");
            }
        }
    }

As I mentioned before, I have Basic Authentication Attribute and somehow I have to use it in my other controller.

UPDATE 2

How can I use OverrideAuthentication and my basic authentication attribute?

public class BasicAuthenticationAttribute : AuthorizationFilterAttribute
    {
        private const string Realm = "My Realm";
        private readonly Func<IUserValidate> _factory;

        public BasicAuthenticationAttribute(Func<IUserValidate> factory)
        {
            _factory = factory;
        }
        ...

UPDATE 3

I tried this in my basic authentication attribute OnAuthorization method;

var authentication = DependencyResolver.Current.GetService<IUserValidate>();
    
                     if (authentication.Login(username, password))
                     {
                         var identity = new GenericIdentity(username);
                         IPrincipal principal = new GenericPrincipal(identity, null);
                         Thread.CurrentPrincipal = principal;
                         if (HttpContext.Current != null) HttpContext.Current.User = principal;

}

There are 2 problems, authentication is null, and somehow authentication token in the attribute is the bearer authentication username/password even though I use basic authentication username/password in the request. It's very weird!

/Get the authentication token from the request header
                 var authenticationToken = actionContext.Request.Headers
                     .Authorization.Parameter;

Any help please?

Thanks in advance.

Upvotes: 0

Views: 16281

Answers (1)

Fake Developer
Fake Developer

Reputation: 25

After long googling, here is how I managed to use both basic authentication and bearer authentication for my different controllers.

In Custom basic authentication Attribute I used dependency and requestScope.GetService.

public class CustomBasicAuthenticationAttribute : AuthorizationFilterAttribute
    {
        [Dependency] public static IUserValidate authentication { get; set; }

        private const string Realm = "My Realm";
        

        public override void OnAuthorization(HttpActionContext actionContext)
        {
            var requestScope = actionContext.Request.GetDependencyScope();

            //If the Authorization header is empty or null
            //then return Unauthorized
            if (actionContext.Request.Headers.Authorization == null)
            {
                actionContext.Response = actionContext.Request
                    .CreateResponse(HttpStatusCode.Unauthorized);
                // If the request was unauthorized, add the WWW-Authenticate header 
                // to the response which indicates that it require basic authentication
                if (actionContext.Response.StatusCode == HttpStatusCode.Unauthorized)
                    actionContext.Response.Headers.Add("WWW-Authenticate",
                        $"Basic realm=\"{Realm}\"");
            }
            else
            {
                //Get the authentication token from the request header
                var authenticationToken = actionContext.Request.Headers
                    .Authorization.Parameter;
                try
                {
                    //Decode the string
                    var decodedAuthenticationToken = Encoding.UTF8.GetString(
                        Convert.FromBase64String(authenticationToken));
                    //Convert the string into an string array
                    var usernamePasswordArray = decodedAuthenticationToken.Split(':');
                    //First element of the array is the username
                    var username = usernamePasswordArray[0];
                    //Second element of the array is the password
                    var password = usernamePasswordArray[1];

                    authentication = requestScope.GetService(typeof(IUserValidate)) as IUserValidate; 

                    if (authentication != null && authentication.Login(username, password))
                    {
                        var identity = new GenericIdentity(username);
                        IPrincipal principal = new GenericPrincipal(identity, null);
                        Thread.CurrentPrincipal = principal;
                        if (HttpContext.Current != null) HttpContext.Current.User = principal;
                    }
                    else
                    {
                        actionContext.Response = actionContext.Request
                            .CreateResponse(HttpStatusCode.Unauthorized);
                    }
                }
                catch
                {
                    actionContext.Response = actionContext.Request
                        .CreateResponse(HttpStatusCode.Unauthorized);
                }
            }
        }
    }

In one of my controller, I added those attributes

        [OverrideAuthentication]
        [CustomBasicAuthentication]

        [HttpPost, Route("purchase")]
        public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
        {
            if (!ModelState.IsValid)
            {
                return BadRequest(ModelState);
            }
...

Now I can use bearer token authentication for ...api/v2/game/abc101/purchase and basic authentication for ...api/v2/game/purchase.

Update

The vital part is the dependency and actionContext.Request.GetDependencyScope();. Without OverrideAuthentication it is working as expected.

Hope this solution helps for others.

Upvotes: 1

Related Questions