2
votes

I've been working through a simple API example, a modified version of the ServiceStack Hello World example with authentication. The goal of the proof of concept is to create an a RESTful API that contains services requiring authentication accessible entirely through Ajax from several different web projects.

I've read the wiki for, and implemented, Authentication and authorization and implementing CORS (many, results [sorry, not enough cred to point to the relevant link]). At this point, my Hello service can authenticate using a custom authentication mechanism which is over-riding CredentialsAuthProvider and a custom user session object. I've created, or borrowed, rather, a simple test application (an entirely separate project to simulate our needs) and can authenticate and then call into the Hello service, passing a name, and receive a 'Hello Fred' response through a single browser session. That is, I can call the /auth/credentials path in the url, passing the username and id, and receive a proper response. I can then update the url to /hello/fred and receive a valid response.

My breakdown in understanding is how to implement the authentication for all ajax calls. My initial login, below, works fine. No matter what I do, my attempt to call the authenticated service via ajax, I either receive a OPTIONS 404 error or Not Found error, or Origin http // localhost:12345 (pseudo-link) is not allowed by Access-Control-Allow-Origin, etc.

Do I need to go this route?

Sorry if this is confusing. I can provide greater details if required, but think this might be sufficient help the knowledgeable to help my lack of understanding.

    function InvokeLogin() {
    var Basic = new Object();
    Basic.UserName = "MyUser";
   Basic.password = "MyPass";

    $.ajax({
        type: "POST",
        contentType: "application/json; charset=utf-8",
        dataType: "json",
        data: JSON.stringify(Basic),
        url: "http://localhost:58795/auth/credentials",
        success: function (data, textStatus, jqXHR) {
                alert('Authenticated! Now you can run Hello Service.');
            },
        error: function(xhr, textStatus, errorThrown) {
            var data = $.parseJSON(xhr.responseText);
            if (data === null)
                alert(textStatus + " HttpCode:" + xhr.status);
            else
                alert("ERROR: " + data.ResponseStatus.Message + (data.ResponseStatus.StackTrace ? " \r\n Stack:" + data.ResponseStatus.StackTrace : ""));
        }
    });
}

EDIT:

Based on the responses and the link provided by Stefan, I've made a couple of changes:

My Config (Note: I'm using custom authentication and session object and that is all working correctly.)

public override void Configure(Funq.Container container)
{
    Plugins.Add(new AuthFeature(() => new CustomUserSession(), 
                new IAuthProvider[] {
                new CustomCredentialsAuthProvider(),
                    }));

    base.SetConfig(new EndpointHostConfig
    {
        GlobalResponseHeaders = {
            { "Access-Control-Allow-Origin", "*" },
            { "Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS" },
            { "Access-Control-Allow-Headers", "Content-Type, Authorization" },
        },
        DefaultContentType = "application/json"
    });

    Plugins.Add(new CorsFeature());
    this.RequestFilters.Add((httpReq, httpRes, requestDto) =>
    {
        //Handles Request and closes Responses after emitting global HTTP Headers
        if (httpReq.HttpMethod == "OPTIONS")
            httpRes.EndRequest();   //   extension method
    });

    Routes
        .Add<Hello>("/Hello", "GET, OPTIONS");


    container.Register<ICacheClient>(new MemoryCacheClient());
    var userRep = new InMemoryAuthRepository();
    container.Register<IUserAuthRepository>(userRep);
}

My Simple Hello Service

[EnableCors]
public class HelloService : IService
{
    [Authenticate]
    public object GET(Hello request)
    {
        Looks strange when the name is null so we replace with a generic name.
        var name = request.Name ?? "John Doe";
        return new HelloResponse { Result = "Hello, " + name };
    }
}

After making the login call, above, my subsequent call the Hello service is now yielding a 401 error, which is progress, though not where I need to be. (The Jquery.support.cors= true is set in my script file.)

function helloService() {
    $.ajax({
        type: "GET",
        contentType: "application/json",
        dataType: "json",
        url: "http://localhost:58795/hello",
        success: function (data, textStatus, jqXHR) {
            alert(data.Result);
        },
        error: function (xhr, textStatus, errorThrown) {
            var data = $.parseJSON(xhr.responseText);
            if (data === null)
                alert(textStatus + " HttpCode:" + xhr.status);
            else
                alert("ERROR: " + data.ResponseStatus.Message +
                    (data.ResponseStatus.StackTrace ? " \r\n Stack:" + data.ResponseStatus.StackTrace : ""));
        }
    });
}

Again, this works in the RESTConsole if I first make the call to /auth/credentials properly and then follow that up with a call to /hello.

FINAL EDIT Following Stefan's advise, below, including many other links, I was finally able to get this working. In addition to Stefan's code, I had to make one additional modification:

Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization"));

On to the next challenge: Updating Jonas Eriksson's CustomAuthenticateAttibute code (which appears to be using an older version of ServiceStack as a couple of functions are no longer available.

THANKS AGAIN STEFAN!!

1
I'm confused. Can you state your question a bit more clearly?zanbri
Without authentication, can you make a simple call in AJAX to check if the CORS works like in this answer ? Can you check your code in AppHost ? In javascript, did you write jQuery.support.cors = true;stefan2410
Thanks for the insight Stefan. It sounds like this person's issues were very similar to what I'm experiencing. I'll work through this example, see what my results are., and update this post. Thanks again.ithank
Can you read my updated answer please ?stefan2410
@ithank, you are right, I updated also the code with the line in your final edit. Thank you too.stefan2410

1 Answers

2
votes

this code works for me, based on the Wiki documentation Custom authentication and authorization

Code is based also in the blog post from Community Resources CORS BasicAuth on ServiceStack with custom authentication

For Basic Authentication, a custom provider

   public class myAuthProvider : BasicAuthProvider
    {
           public myAuthProvider() : base() { }

       public override bool TryAuthenticate(IServiceBase authService, string userName, string  password)
    {
        //Add here your custom auth logic (database calls etc)
        //Return true if credentials are valid, otherwise false
        if (userName == "admin" && password == "test")
                      return true;
         else
               return false;

    }

    public override void OnAuthenticated(IServiceBase authService, IAuthSession session, IOAuthTokens tokens, Dictionary<string, string> authInfo)
    {
        //Fill the IAuthSession with data which you want to retrieve in the app 
       //  the base AuthUserSession properties e.g
       session.FirstName = "It's me";
        //...   
       //  derived CustomUserSession properties e.g
        if(session is CustomUserSession)
       ((CustomUserSession) session).MyData = "It's me";
        //...
        //Important: You need to save the session!
        authService.SaveSession(session, SessionExpiry);
    }
}

public class CustomUserSession : AuthUserSession
{

    public string MyData { get; set; }
}

In AppHost

     using System.Web;
     using ServiceStack;     // v.3.9.60  httpExtensions methods, before in ServiceStack.WebHost.Endpoints.Extensions;

     using ....

AppHost.Configure

     public override void Configure(Container container)
      {
          SetConfig(new ServiceStack.WebHost.Endpoints.EndpointHostConfig
          {
               DefaultContentType = ContentType.Json 
                 ..
               //   remove GlobalResponseHeaders  because CordFeature adds the CORS headers to  Config.GlobalResponseHeaders

            }); 
       Plugins.Add(new CorsFeature(allowedHeaders: "Content-Type, Authorization")); //Registers global CORS Headers
        this.RequestFilters.Add((httpReq, httpRes, requestDto) =>
        {
            if (httpReq.HttpMethod == "OPTIONS")
                  httpRes.EndRequestWithNoContent();   // v 3.9.60 httpExtensions method before httpRes.EndServiceStackRequest();  

        });

          //Register all Authentication methods you want to enable for this web app.
            Plugins.Add(new AuthFeature(() => new  CustomUserSession(), // OR the AuthUserSession 
                new IAuthProvider[] {
                new  myAuthProvider(),   
              }) { HtmlRedirect = null }); //  Redirect on fail 

HtmlRedirect answer

             Routes.Add<TestRequest>("/TestAPI/{Id}", "POST,GET, OPTIONS");
        ....
      } 

In Service

           [Authenticate]
          public class TestAPI : Service
            {    
                 ...
            }

in javascript

     jQuery.support.cors = true;

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

Login first

           function Authenticate() {

              $.ajax({
              type: 'Post',
              contentType: 'application/json',
              url: serverIP + 'Auth',
              cache: false,
              async: false,
              data: {},
              dataType: "json",
              beforeSend: function (xhr) {
                  xhr.setRequestHeader("Authorization", make_base_auth(username, password));
              },
              success: function (response, status, xhr) {
                  localStorage.sessionId = data.SessionId;
                  var UserName  = response.userName;
              },
              error: function (xhr, err) {
                  alert(err);
              }
          });
      }  

and request

         function DoTest() {
              var TestRequest = new Object();
              TestRequest.name = "Harry Potter";             
              TestRequest.Id = 33;
             var username = "admin";
             var password = "test"; 

             $.ajax({
              type: 'Post',
              contentType: 'application/json',
              cache: false,
              async: false,
              url: serverIP + '/TestAPI/'+ TestRequest.Id,
              data: JSON.stringify(TestRequest),
              dataType: "json",                  
              beforeSend: function (xhr) {                    
                xhr.setRequestHeader("Session-Id", localStorage.sessionId);
              },
           success: function (response, status, xhr) {
                  var s= response.message;      
              },
              error: function (xhr, err) {
                  alert(xhr.statusText);
              }
          });
      }

these questions here and here are helpful.

Also this answer for CredentialsAuthProvider, in case we can use cookies and sessions.