2

I'm trying to create an MVC-based site providing some services via WebApi, to both local pages and external clients, thus requiring JSONP to avoid the same origin policy error. The problem is that the site is using Basic authentication, which as I learnt from other posts here cannot play with JSONP. I tried to inject user:pass in the URL, as suggested in the post How do I make a JSONP call with JQuery with Basic Authentication?, but this does not work and the server returns an unauthorized code. Also, I tried just making the call without injecting, as I would be fine in entering username and password in the browser: the browser asks me for the credentials as expected, but then for some reason they are refused, ending into an unauthorized code again. Yet the credentials are OK, as I can confirm by successfully running exactly the same code from the same domain. Could anyone tell me what's wrong with my code?

My MVC WebApi controller action is like:

[BasicAuthorize(Roles = "administrator,customer,trial")]
public class TextApiController : ApiController
{
      // ...

    public SomeResult Get([FromUri] SomeParams p)
    {
          // some processing which returns a SomeResult object
          //...
    }
}

where the BasicAuthorize attribute is a class I modified from http://kevin-junghans.blogspot.it/2013/02/mixing-forms-authentication-basic.html, as follows:

[AttributeUsageAttribute(AttributeTargets.Class |
    AttributeTargets.Method, Inherited = true,
    AllowMultiple = true)]
public sealed class BasicAuthorizeAttribute : AuthorizeAttribute
{
    static private string DecodeFrom64(string sEncodedData)
    {
        byte[] encodedDataAsBytes = Convert.FromBase64String(sEncodedData);
        return Encoding.ASCII.GetString(encodedDataAsBytes);
    }

    static private bool GetUserNameAndPassword(HttpActionContext context,
        out string sUserName,
        out string sPassword,
        out bool bCookieAuthorization)
    {
        bCookieAuthorization = false;
        bool bSuccess = false;
        sUserName = sPassword = "";
        IEnumerable<string> headerVals;

        if (context.Request.Headers.TryGetValues("Authorization", out headerVals))
        {
            try
            {
                string sAuthHeader = headerVals.First();
                string[] authHeaderTokens = sAuthHeader.Split();

                if (authHeaderTokens[0].Contains("Basic"))
                {
                    string sDecoded = DecodeFrom64(authHeaderTokens[1]);
                    string[] aPairMembers = sDecoded.Split(new[] { ':' });
                    sUserName = aPairMembers[0];
                    sPassword = aPairMembers[1];
                } 
                else
                {
                    if (authHeaderTokens.Length > 1)
                        sUserName = DecodeFrom64(authHeaderTokens[1]);
                    bCookieAuthorization = true;
                } 

                bSuccess = true;
            }
            catch
            {
                bSuccess = false;
            }
        } 

        return bSuccess;
    }

    static private bool Authenticate(HttpActionContext actionContext,
        out string sUserName)
    {
        bool bIsAuthenticated = false;
        string sPassword;
        bool bCookieAuthorization;

        if (GetUserNameAndPassword(actionContext,
            out sUserName, out sPassword, out bCookieAuthorization))
        {
            // if the header tells us we're using Basic auth then log the user in
            if (!bCookieAuthorization)
            {
                if (WebSecurity.Login(sUserName, sPassword, true))
                    bIsAuthenticated = true;
                else
                    WebSecurity.Logout();
            } 
            // else get authentication from web security
            else
            {
                if (WebSecurity.IsAuthenticated) bIsAuthenticated = true;
                sUserName = WebSecurity.CurrentUserName;
            } 
        } 
        else actionContext.Response =
            new HttpResponseMessage(HttpStatusCode.BadRequest);

        return bIsAuthenticated;
    }

    private bool IsAuthorized(string sUserName)
    {
        SimpleRoleProvider roles =
            (SimpleRoleProvider)System.Web.Security.Roles.Provider;
          string[] aRoles = Roles.Split(new[] {','});

        return (aRoles.Any(sRole => roles.IsUserInRole(sUserName, sRole)));
    }

    public override void OnAuthorization(HttpActionContext actionContext)
    {
        string sUserName;

        if (Authenticate(actionContext, out sUserName))
        {
            if (!IsAuthorized(sUserName))
                actionContext.Response = new HttpResponseMessage(HttpStatusCode.Forbidden);
        } 
        else
        {
            actionContext.Response = new HttpResponseMessage(HttpStatusCode.Unauthorized);
        } 
    }
}

My client code is a simple HTML page with some Javascript (using jQuery), like:

...
<form>
    <fieldset>
        <legend></legend>
        <ol>
            <li>
                input text
                <input type="text" id="input"/>
            </li>
            <li>
                username
                <input type="text" id="user"/>
            </li>
            <li>
                password
                <input type="password" id="password"/>
            </li>
            <li><a href="#" id="apip">API: JSONP</a></li>
        </ol>
    </fieldset>
</form>
<div id="result"></div>
<script>
    function getAuthorizationHeader(username, password) {
        "use strict";
        var authType;

        if (password == "") {
            authType = "Cookie " + $.base64.encode(username);
        }
        else {
            var up = $.base64.encode(username + ":" + password);
            authType = "Basic " + up;
        };
        return authType;
    };

    function ajaxSuccessHandler(data) {
        "use strict";
        $("#result").text(data);
    };

    function ajaxErrHandler(jqXHR, textStatus, errorThrown) {
        "use strict";
        $("#result").text(errorThrown + " : " + textStatus);
    }

    $(function () {
        "use strict";

        $("#apip").click(function () {
            "use strict";
            var text = $("#input").val();
            $.ajax({
                url: "https://somesiteurl.com/api/wordapi?Text=" + encodeURIComponent(text),
                dataType: "jsonp",
                type: "GET",
                beforeSend: function (xhr) {
                    xhr.setRequestHeader("Authorization", getAuthorizationHeader($("#user").val(), $("#password").val()));
                },
                success: ajaxSuccessHandler,
                error: ajaxErrHandler
            });
        });
    });
</script>

CORS

Sorry for the late response... I'm trying with CORS as suggested, but I'm surely missing something obvious as the headers sent by my client do not include Origin. Here is what I did, by using the library from http://brockallen.com/2012/06/28/cors-support-in-webapi-mvc-and-iis-with-thinktecture-identitymodel/ :

  1. I create a new MVC4 internet application to test this scenario, and add Thinktecture.IdentityModel using NuGet.

  2. in App_Start I create this CorsConfig class:

static public class CorsConfig
{
    public static void RegisterCorsForWebApi(HttpConfiguration httpConfig)
    {
        WebApiCorsConfiguration corsConfig = new WebApiCorsConfiguration();

    // this adds the CorsMessageHandler to the HttpConfiguration’s 
    // MessageHandlers collection
    corsConfig.RegisterGlobal(httpConfig);

    corsConfig
        .ForResources("Products")
        .ForOrigins("http://hello.net")
        .AllowAll();
}

public static void RegisterCorsForMvc(MvcCorsConfiguration corsConfig)
{
    corsConfig
        .ForResources("Products.GetProducts")
        .ForOrigins("http://hello.net")
        .AllowAll();
}

}

  1. in Global.asax.cs I call both methods of this class.

  2. in web.config add:

  3. I create a simple action method in a MVC controller returning some JSON. I plan to decorate with [Authorize] later, once the call is placed correctly on the client side, so I can test the authentication (and authorization, adding Roles).

  4. in a view, I call my method like:

var text = $("#input").val();
var json = "{'text': " + JSON.stringify(text) + "}";
$.ajax({
    url: "/Home/GetSomeJson",
    dataType: "json",
    data: json,
    type: "GET",
    beforeSend: function (xhr) {
        xhr.withCredentials = true;
    },
    crossDomain: true,
    username: $("#user").val(),
    password: $("#password").val(),
    success: ajaxSuccessHandler,
    error: ajaxErrHandler
});
Yet, examining the headers I see no origin. Also, is this the correct way of passing credentials (of course in real world this would be HTTPS) for a CORS call to MVC action/WebApi?

Community
  • 1
  • 1
Naftis
  • 4,393
  • 7
  • 63
  • 91

1 Answers1

2

Why dont you CORS enable your web api service instead of using JSONP? . This is a great post explaining how to enable CORS support in Web API

Nishanth Nair
  • 2,975
  • 2
  • 19
  • 22