5

I have implemented a Basic Authentication Middleware for Katana (Code below).

(My client is hosted on a cross domain then the actually API).

The browser can skip the preflight request if the following conditions are true:

The request method is GET, HEAD, or POST, and The application does not set any request headers other than Accept, Accept-Language, Content-Language, Content-Type, or Last-Event-ID, and The Content-Type header (if set) is one of the following: application/x-www-form-urlencoded multipart/form-data text/plain

In javascript I set the authentication header( with jquery, beforeSend) on all requests for the server to accept the requests. This means that above will send the Options request on all requests. I dont want that.

function make_base_auth(user, password) {
    var tok = user + ':' + password;
    var hash = Base64.encode(tok);
    return "Basic " + hash;
}

What would I do to get around this? My idea would be to have the user information stored in a cookie when he has been authenticated.

I also saw in the katana project that are a Microsoft.Owin.Security.Cookies - is this maybe what i want instead of my own basic authentication?

BasicAuthenticationMiddleware.cs

using Microsoft.Owin;
using Microsoft.Owin.Logging;
using Microsoft.Owin.Security.Infrastructure;
using Owin;

namespace Composite.WindowsAzure.Management.Owin
{
    public class BasicAuthenticationMiddleware : AuthenticationMiddleware<BasicAuthenticationOptions>
    {
        private readonly ILogger _logger;

        public BasicAuthenticationMiddleware(
           OwinMiddleware next,
           IAppBuilder app,
           BasicAuthenticationOptions options)
            : base(next, options)
        {
            _logger = app.CreateLogger<BasicAuthenticationMiddleware>();
        }

        protected override AuthenticationHandler<BasicAuthenticationOptions> CreateHandler()
        {
            return new BasicAuthenticationHandler(_logger);
        }
    }
}

BasicAuthenticationHandler.cs

using Microsoft.Owin.Logging;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using System;
using System.Text;
using System.Threading.Tasks;

namespace Composite.WindowsAzure.Management.Owin
{
    public class BasicAuthenticationHandler : AuthenticationHandler<BasicAuthenticationOptions>
    {
        private readonly ILogger _logger;

        public BasicAuthenticationHandler(ILogger logger)
        {
            _logger = logger;
        }
        protected override Task ApplyResponseChallengeAsync()
        {
            _logger.WriteVerbose("ApplyResponseChallenge");
            if (Response.StatusCode != 401)
            {
                return Task.FromResult<object>(null);
            }

            AuthenticationResponseChallenge challenge = Helper.LookupChallenge(Options.AuthenticationType, Options.AuthenticationMode);

            if (challenge != null)
            {
                Response.Headers.Set("WWW-Authenticate", "Basic");
            }

            return Task.FromResult<object>(null);
        }
        protected override async Task<AuthenticationTicket> AuthenticateCoreAsync()
        {
            _logger.WriteVerbose("AuthenticateCore");

            AuthenticationProperties properties = null;

            var header = Request.Headers["Authorization"];

            if (!String.IsNullOrWhiteSpace(header))
            {
                var authHeader = System.Net.Http.Headers.AuthenticationHeaderValue.Parse(header);

                if ("Basic".Equals(authHeader.Scheme, StringComparison.OrdinalIgnoreCase))
                {
                    string parameter = Encoding.UTF8.GetString(Convert.FromBase64String(authHeader.Parameter));
                    var parts = parameter.Split(':');
                    if (parts.Length != 2)
                        return null;

                    var identity = await Options.Provider.AuthenticateAsync(userName: parts[0], password: parts[1], cancellationToken: Request.CallCancelled);
                    return new AuthenticationTicket(identity, properties);
                }
            }

            return null;
        }
    }
}

Options.Provider.AuthenticateAsync validated the username/password and return the identity if authenticated.

Specifications

What I am trying to solve is: I have a Owin Hosted WebAPI deployed with N Azure Cloud Services. Each of them are linked to a storage account that holds a list of username/hashed passwords.

From my client I am adding any of these N services to the client and can then communicate with them by their webapis. They are locked down with authentication. The first step is to validate the users over basic authentication scheme with the list provided above. After that, I hope its easy to add other authentication schemes very easy as of the Owin, UseWindowsAzureAuthentication ect, or UseFacebookAuthentication. (I do have a challenge here, as the webapi do not have web frontend other then the cross domain site that adds the services).

If your good at Katana and want to work alittle with me on this, feel free to drop me a mail at pks@s-innovations.net. I will provide the answer here at the end also.

Update

Based on answer I have done the following:

app.UseCookieAuthentication(new CookieAuthenticationOptions
{
    AuthenticationType = "Application",
    AuthenticationMode = AuthenticationMode.Active,
    LoginPath = "/Login",
    LogoutPath = "/Logout",
    Provider = new CookieAuthenticationProvider
    {
        OnValidateIdentity = context =>
        {
            //    context.RejectIdentity();
            return Task.FromResult<object>(null);
        },
        OnResponseSignIn = context =>
        {

        }
    }
});

app.SetDefaultSignInAsAuthenticationType("Application");

I assume that it has to be in AuthenticationMode = Active, else the Authorize attributes wont work?

What exactly needs to be in my webapi controller to do the exchange for a cookie?

public async Task<HttpResponseMessage> Get()
{
    var context = Request.GetOwinContext();
    //Validate Username and password
    context.Authentication.SignIn(new AuthenticationProperties()
    {
        IsPersistent = true
    },
    new ClaimsIdentity(new[] { new Claim(ClaimsIdentity.DefaultNameClaimType, "MyUserName") }, "Application"));

    return Request.CreateResponse(HttpStatusCode.OK);
}

Is above okay?

Current Solution

I have added my BasicAuthenticationMiddleware as the active one, added the above CookieMiddleware as passive.

Then in the AuthenticateCoreAsync i do a check if I can login with the Cookie,

 var authContext = await Context.Authentication.AuthenticateAsync("Application");
            if (authContext != null) 
                return new AuthenticationTicket(authContext.Identity, authContext.Properties);

So I can now exchange from webapi controller a username/pass to a cookie and i can also use the Basic Scheme directly for a setup that dont use cookies.

Community
  • 1
  • 1
Poul K. Sørensen
  • 16,950
  • 21
  • 126
  • 283

1 Answers1

2

If web api and javascript file are from different origins and you have to add authorization header or cookie header to the request, you cannot prevent browser from sending preflight request. Otherwise it will cause CSRF attack to any protected web api.

You can use OWIN Cors package or Web API Cors package to enable CORS scenario, which can handle the preflight request for you.

OWIN cookie middleware is responsible for setting auth cookie and verify it. It seems to be what you want.

BTW, Basic auth challenge can cause browser to pop up browser auth dialog, which is not expected in most of the web application. Not sure if it's what you want. Instead, using form post to send user name and password and exchange them with cookie is what common web app does.

If you have VS 2013 RC or VWD 2013 RC installed on your machine, you can create an MVC project with Individual auth enabled. The template uses cookie middleware and form post login. Although it's MVC controller, you can simply convert the code to Web API.

[Update] Regarding preflight request, it will be sent even with cookie header according to the spec. You may consider to add Max Age header to make it be cached on the browser. JSONP is another option which doesn't require preflight.

[Update2] In order to set cookie by owin middleware, please use the following sample code.

var identity = new ClaimsIdentity(CookieAuthenticationDefaults.ApplicationAuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, "Test"));
AuthenticationManager.AuthenticationResponseGrant = new AuthenticationResponseGrant(identity, new AuthenticationProperties()
{
    IsPersistent = true
});
Hongye Sun
  • 3,868
  • 1
  • 25
  • 18
  • THanks for the comments. I am looking into the cookies auth things. I are looking at the MVC project also, but i am getting stuck at converting it to the webapi. MVC uses the Owin.Identity stuff with CheckPasswordAndSignInAsync - i dont have that. I will update my question with a little more info in a few seconds. – Poul K. Sørensen Sep 16 '13 at 17:23
  • and i already have cors working. I just didnt want to send two requests everything because of the basic authentication, thats why i started looking into the cookies instead. – Poul K. Sørensen Sep 16 '13 at 17:29
  • I would like to have both options, cookies and basic auth. I have a commandline tool also that need access to the webapi. Not sure if both should have mode= active, or just cookies. – Poul K. Sørensen Sep 16 '13 at 17:40
  • I updated my answer to respond to your comments. Using cookie won't help on avoiding preflight request. You may consider preflight cache or JSONP as options. – Hongye Sun Sep 16 '13 at 19:03
  • thanks. The last code snippet you posted. I should use that instead of Context.Authentication.SignIn, and will i still be able to do AuthenticateAsync to read it again? – Poul K. Sørensen Sep 16 '13 at 19:12
  • The code is to tell cookie middleware to set cookie for the identity. If you set the middleware as active, you don't need to call AuthentityAsync to get the identity from request. ApiController.User property will have the user identity. – Hongye Sun Sep 16 '13 at 19:44
  • Can both my Basic and Cookie Middleware be active? – Poul K. Sørensen Sep 16 '13 at 20:01
  • In my Basic Middleware, AuthenticateCoreAsync - How do i load the identity from cookie if its set then? I want to load it from cookie if its set and if not the continue with the Basic. (reason: Cookie load is faster then my basic user validation as its roundtripping data store. – Poul K. Sørensen Sep 16 '13 at 20:04
  • You can set both middlewares as active. It means both middlewares will actively authenticate request and challenge if it is unauthorized. In the AuthetnicateCoreAsync, you can check OwinContext.Request.User to see if the request has already been authenticated. Note that you need to register Cookie middleware before your custom middleware. – Hongye Sun Sep 16 '13 at 21:26
  • Almost there now. Things start to work, Cookies Middleware authenticates before my owin (is the order always the same as they are added in startup?). Minor issue left i think, my custom authentication also tries to authenticate the "slow" basic on the preflight options request. Thank you for helping out. – Poul K. Sørensen Sep 16 '13 at 21:39
  • Both active works almost great. Only problem is that the CookieMiddleware takes precedence over my middleware and redirects the browser instead where I would like my middleware to do the Response.Headers.Set("WWW-Authenticate", "Basic"); – Poul K. Sørensen Sep 16 '13 at 21:59
  • The cookie middleware challenge only happens if LoginPath option is not null. The default UseSignInCookies extension method will set default value to "Account/Login". You can use extension method UseCookieAuthentication instead and set LoginPath as null. – Hongye Sun Sep 16 '13 at 22:15