1
\$\begingroup\$

In my asp.net web API, I have a couple of controllers. In one of my controllers, I am using basic authentication and in another one, I am using bearer token authentication. It is working as it is expected, I wonder if are there any improvements to make it better. So I would be glad if you can share your comments.

Controller A -> Basic Authentication -> 401 if Basic Authentication fails

Controller B -> Bearer Token Authentication -> 401 if Bearer Token Authentication fails

Owin 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());
    }
}

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;
        // Web API routes
        config.MapHttpAttributeRoutes();

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

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.");
            context.Rejected();
            return;
        }    
        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.");
        }
    }
}

Basic Authentication Attribute

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);
            }
        }
    }
}

Controller A

[RoutePrefix("api/v2/game")]
public class LoadTestController : ApiController
{
    private readonly IGameServicesTest _gameServices;
     
    public LoadTestController(IGameServicesTest gameServices)
    {
        _gameServices = gameServices;
    }

    [CustomBasicAuthentication]

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

Controller B

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

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

    [HttpPost]
    
    [Route("purchase")]
    public async Task<IHttpActionResult> PurchaseGame(RequestDto game)
    {
        if (!ModelState.IsValid) return BadRequest(ModelState);
        ...
    }
    ...
}
\$\endgroup\$
2
  • \$\begingroup\$ From what aspect are you looking for review? Legibility, maintenance, security or something else? \$\endgroup\$ Commented Feb 21, 2022 at 14:15
  • \$\begingroup\$ mostly maintenance and security perspectives. \$\endgroup\$
    – raysefo
    Commented Feb 21, 2022 at 15:26

1 Answer 1

1
\$\begingroup\$

In this post I will focus only on the security aspect.

Authentication vs Authorization

  • Based on your question I'm not sure that you have a clear understanding about these concepts
    • Authentication: verify that the requester is that person/entity who (s)he/it claims
    • Authorization: verify that the authenticated identity has the required privilege to execute the requested action
  • In case of WebApi we have two core interfaces:
    • IIdentity: represents the authenticated person/entity
    • IPrinciple: represents the security context via the identity and its related roles
  • Nowadays we usually use the ClaimsPrinciple where facts about the identity and his/her roles are represented as claims

Basic Authentication

  • First of all you should not use this protocol at all
    • It is not secure by any means it is just an obfuscated way to pass credentials
    • OWASP also suggests to try to prevent it
  • The only viable place where it could make sense is intranet (not over the internet)
    • But here you should also consider to use it over TLS (encrypted channel)
  • You have implemented your Basic Auth handler as AuthorizationFilterAttribute

Token based Authentication

  • In WebAPI you can only achieve it via OAuthAuthorizationServerProvider
    • Which is bad since OAuth 2 is an authorization protocol not an authentication one
      • OpenId Connect is the related authentication protocol
      • In OAuth 2 there are an authorzation server and a resource server entities
      • But with this .NET class you have to implement both sides :(
        • I have seen a dozen of implementations where the authentication was part of the GrantResourceOwnerCredentials method and the ValidateClientAuthentication simply called the context.Validated
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.