1

I'm writing my first Web API 2. I'm performing authorization using a custom HttpMessageHandler. However, my controller needs to know the username specified in the credentials.

Researching this, it appears that ApiController does not have a Controller.HttpContext property. And I see there are potential issues accessing HttpContext.Current. So while I am actually able to store the username in HttpContext.Current.Items in my HttpMessageHandler and then access that information from my controller, I'm not sure that will always be reliable.

I also saw recommendations to use the RequestContext.Principal property; however, I could not find the current request's username anywhere in this data.

How can my controller reliably get the username for the current request?

NOTE: I refer to the username but in this case the actual user is another piece of software calling the API. The "username" reflects the software that is making the call.

Jonathan Wood
  • 65,341
  • 71
  • 269
  • 466
  • Did you [read the documentation](https://www.asp.net/web-api/overview/security/authentication-and-authorization-in-aspnet-web-api)? – mason Jan 20 '17 at 18:55
  • @mason: Yes, but I missed where it answers my question. – Jonathan Wood Jan 20 '17 at 18:58
  • I did read your question, and I saw this in the documentation: "Within a controller action, you can get the current principal from the ApiController.User property. For example, you might filter a list of resources based on the user name, returning only those resources that belong to that user." – mason Jan 20 '17 at 18:58
  • Great. It was kind of hard to spot in there. – mason Jan 20 '17 at 18:59
  • @mason: Yes, thank you. That addresses the question. But that returns the same information returned by `RequestContext.Principle` and I have not been able to find the actual username in there. At least, not with the way I'm doing authorization. – Jonathan Wood Jan 20 '17 at 19:01
  • Perhaps you need to create your own authorization filter? Or your authentication layer isn't setting the correct properties? – mason Jan 20 '17 at 19:02
  • See the answers on [this question](http://stackoverflow.com/questions/15204257/how-to-implement-httpmessagehandler-in-web-api). It shows how to create a principal and set it for later in the pipeline. – mason Jan 20 '17 at 19:03
  • @mason: Interesting. Obviously, I don't have the problem the OP had but he does show some code that creates the identity. I'll give that a try. – Jonathan Wood Jan 20 '17 at 19:05
  • @mason: And, yes, that code does work. It still accesses `HttpContext.Current`, so I need to reconcile that with some concerns I've read about with that. But it does work. Thanks. – Jonathan Wood Jan 20 '17 at 19:11
  • No, that's not an issue. Check the documentation again. Only set `HttpContext.Current.User` if `HttpContext.Current` is not null. This will keep compatibility with self-hosting while still setting up the HttpContext up properly if you're running from within ASP.NET pipeline. – mason Jan 20 '17 at 19:12
  • @mason: The concerns I read about were related to threading. But perhaps that's not an issue in my `HttpMessageHandler`. Need to do a bit more reading. – Jonathan Wood Jan 20 '17 at 19:14
  • @JonathanWood Who is your client? *For example, Angular, IOS device.* – Win Jan 20 '17 at 19:31
  • @Win: In this case, the client is any one of our customer websites (which we write and maintain), which are written (unfortunately) in WebForms. – Jonathan Wood Jan 20 '17 at 19:37
  • @JonathanWood How did they give you credentials? *For example, Basic Authentication* – Win Jan 20 '17 at 19:45
  • @Win: Well, that part is what I am developing. But currently basic authentication seems appropriate, where the username identifies the software contacting us and the password is a special key. – Jonathan Wood Jan 20 '17 at 19:46

1 Answers1

2

@Win: Well, that part is what I am developing. But currently basic authentication seems appropriate, where the username identifies the software contacting us and the password is a special key

Here is the sample code for BasicAuthenticationMessageHandler which uses message handler to support HTTP Basic Authentication.

You can read more at Page 121 of ASP.NET Web API 2: Building a REST Service from Start to Finish.

IBasicSecurityService

public interface IBasicSecurityService
{
    bool SetPrincipal(string username, string password);
}

BasicSecurityService

public class BasicSecurityService : IBasicSecurityService
{
    public bool SetPrincipal(string username, string password)
    {
        // Get user from database
        var user = GetUser(username);
        IPrincipal principal = null;
        if (user == null || (principal = GetPrincipal(user)) == null)
        {
            // System could not validate user
            return false;
        }

        Thread.CurrentPrincipal = principal;
        if (HttpContext.Current != null)
        {
            HttpContext.Current.User = principal;
        }

        return true;
    }

    public virtual IPrincipal GetPrincipal(User user)
    {
        var identity = new GenericIdentity(user.Username, Constants.SchemeTypes.Basic);

        identity.AddClaim(new Claim(ClaimTypes.GivenName, user.Firstname));
        identity.AddClaim(new Claim(ClaimTypes.Surname, user.Lastname));
        // Get authroized roles and add them as Role Claim.
        identity.AddClaim(new Claim(ClaimTypes.Role, "Manager"));

        return new ClaimsPrincipal(identity);
    }
}

BasicAuthenticationMessageHandler

public class BasicAuthenticationMessageHandler : DelegatingHandler
{
    public const char AuthorizationHeaderSeparator = ':';
    private const int UsernameIndex = 0;
    private const int PasswordIndex = 1;
    private const int ExpectedCredentialCount = 2;

    private readonly IBasicSecurityService _basicSecurityService;

    public BasicAuthenticationMessageHandler(IBasicSecurityService basicSecurityService)
    {
        _basicSecurityService = basicSecurityService;
    }

    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request,
        CancellationToken cancellationToken)
    {
        if (HttpContext.Current.User.Identity.IsAuthenticated)
        {
            // Already authenticated; passing on to next handler...
            return await base.SendAsync(request, cancellationToken);
        }

        if (!CanHandleAuthentication(request))
        {
            // Not a basic auth request; passing on to next handler...
            return await base.SendAsync(request, cancellationToken);
        }

        bool isAuthenticated;
        try
        {
            isAuthenticated = Authenticate(request);
        }
        catch (Exception e)
        {
            // Failure in auth processing
            return CreateUnauthorizedResponse();
        }

        if (isAuthenticated)
        {
            var response = await base.SendAsync(request, cancellationToken);
            return response;
        }

        return CreateUnauthorizedResponse();
    }

    public bool CanHandleAuthentication(HttpRequestMessage request)
    {
        return (request.Headers != null
                && request.Headers.Authorization != null
                && request.Headers.Authorization.Scheme.ToLowerInvariant() == Constants.SchemeTypes.Basic);
    }

    public bool Authenticate(HttpRequestMessage request)
    {
        // Attempting to authenticate...
        var authHeader = request.Headers.Authorization;
        if (authHeader == null)
        {
            return false;
        }

        var credentialParts = GetCredentialParts(authHeader);
        if (credentialParts.Length != ExpectedCredentialCount)
        {
            return false;
        }

        return _basicSecurityService.SetPrincipal(credentialParts[UsernameIndex], credentialParts[PasswordIndex]);
    }

    public string[] GetCredentialParts(AuthenticationHeaderValue authHeader)
    {
        var encodedCredentials = authHeader.Parameter;
        var credentialBytes = Convert.FromBase64String(encodedCredentials);
        var credentials = Encoding.ASCII.GetString(credentialBytes);
        var credentialParts = credentials.Split(AuthorizationHeaderSeparator);
        return credentialParts;
    }

    public HttpResponseMessage CreateUnauthorizedResponse()
    {
        var response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        response.Headers.WwwAuthenticate.Add(new AuthenticationHeaderValue(Constants.SchemeTypes.Basic));
        return response;
    }
}
Win
  • 61,100
  • 13
  • 102
  • 181
  • Thanks, I'll look this over closely, and perhaps I'll pick up the book. (Are you involved in it?) I did get some help setting up my current code and can't say I'm ready to abandon it. It seems to work just fine, except that I just today realized that I really need to be able to access the username value from my controller. – Jonathan Wood Jan 20 '17 at 20:12
  • I read the book a couple of years ago, and implemented few projects with that approach *successfully*. Although I did not use some frameworks and methodologies used in the book *such as NHibernate, Unit of Work, and knockout.js*, all and all I learned a lot of tip and tricks, and new features of Web API 2. – Win Jan 20 '17 at 20:22