#1 Re: mORMot 1 » Javascript authentication » 2011-11-11 15:22:33

Another feature, I used in my project: I want inform the user if it cannot update/delete record and why.
There is my modifications (not whole modifications shown, only main points):
SQLite3Commons.pas:

function TSQLRest.RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer; Action: TSQLEvent; [b]var ErrorMsg: RawUTF8[/b]): boolean; virtual;

function TSQLRestServer.URI(const url, method, SentData: RawUTF8;
var
...
    [b]ErrorMsg: RawUTF8;[/b]
begin
...
    if MethodUp='DELETE' then begin     // DELETE
      // ModelRoot/TableName/ID to delete a member
      if (Table<>nil) and (ID>0) then
      if not [b]RecordCanBeUpdated(Table,ID,seDelete, ErrorMsg)[/b] then // HTTP Forbidden
      begin
        result.Lo := 401;
        [b]Resp := JSONEncode(['errorCode','401', 'errorText', ErrorMsg]);[/b]
      end
      else 
      if not (TableIndex in RestAccessRights^.DELETE) then // check User
      begin
        result.Lo := 401;
        [b]Resp := JSONEncode(['errorCode','401', 'errorText', 'no access']);[/b]
      end
      else
...

Code from my project that uses this feature:

type
  TMainServer = class(TSQLRestServerDB)
  protected
    function RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer; Action: TSQLEvent; var ErrorMsg: RawUTF8): boolean; override;
  end;
...
function TMainServer.RecordCanBeUpdated(Table: TSQLRecordClass; ID: integer;
  Action: TSQLEvent; var ErrorMsg: RawUTF8): boolean;
begin
  Result := True;
  if (Table = TSQLBranches) and (ID = 1) and (Action = seDelete) then
  begin
    ErrorMsg := 'You cannot delete main branch!';
    Result := False;
  end;
end;

Perhaps more information (like SentData from URI?) should be passed to RecordCanBeUpdated for a more flexible implementation of business logic?

Example client-side code (javascript):

function globalErrorHandler(jqXHR, textStatus, errorThrown) {
    $.unblockUI();
    if (textStatus == "timeout")
        create_notify("msgs", { title:'Timeout', text:'Sorry, server is down at the moment. Try again later', icon:'server_down.gif' });
    else
    {
        switch (jqXHR.status) {
            case 401:
                if (textStatus == 'no access')
                {
                    create_notify("msgs", { title:'Access denied', text:'You do not have permission to use this function', icon:'error48.png' });
                    setTimeout(function() {
                        document.location = '/pages/main_'+getSet('SESSION_GROUP')+'.htm';
                    }, 2000);
                }
                else
                {
                    // custom error from server (from RecordCanBeUpdated)
                    eval('var response = ('+jqXHR.responseText+')');
                    create_notify("msgs", { title:'Record cannot be updated', text:response.errorText, icon:'error48.png' });
                }
            break;

            case 403:
                create_notify("msgs", { title:'Authentication error', text:'You do not authenticated and will be redirected to login page', icon:'error48.png' });
                setTimeout(function() {
                    document.location = '/index.htm';
                }, 2000);
            break;
            default: create_notify("msgs", { title:'Unknown error: ' + jqXHR.status, text:'Unknown error raised: ' + textStatus + ': ' + errorThrown, icon:'error48.png' });
        }
    }
    jqXHR.abort();
    return false;
}

$.ajaxSetup({error: globalErrorHandler});

#2 Re: mORMot 1 » Javascript authentication » 2011-11-11 14:15:33

Thanks for the feedback smile I will do it like Sir Rufo wrote.

#3 Re: mORMot 1 » Javascript authentication » 2011-11-11 13:35:15

TSQLBranch is just a example of what I want smile Besides, I want a few more fields in TSQLAuthUser class, like LastName, FirstName, Birthdate, etc... smile I can (surely) use a dynamic array of record with these fields. But then lost OOP-flexibility. Can you give me an example of TSQLRestServer.Create() constructor modification? Only a few lines of code that I understand the direction.

#4 Re: mORMot 1 » Javascript authentication » 2011-11-11 11:49:58

And my question: is there a way to override TSQLAuthUser model (i.e. TSQLMyAuthUser, without patching source code of mORMot, just native OOP) to add relationship with TSQLBranch model? I need a "One to many" relationship between two tables: AuthUser and Branches (ID, Name, Address). I don't want to use TSQLAuthUser.Data property, because too many code for work with simply integer ID-field smile

#5 Re: mORMot 1 » Javascript authentication » 2011-11-11 11:34:22

Code from my working project (it uses jQuery):
config.js:

// configuration section

// URL of application server
var SERVER_URL = "http://localhost";
// model root of application server
var SERVER_ROOT = "r";

// end of configuration section

var MAIN_URL = SERVER_URL + '/' + SERVER_ROOT;

shared.js:

function d2h(d, padding) {
    var hex = Number(d).toString(16);
    padding = typeof (padding) === "undefined" || padding === null ? padding = 2 : padding;

    while (hex.length < padding) {
        hex = "0" + hex;
    }
    return hex;
}

function prf(name) {
    return 'loans_' + name;
}

function getSet(name) {
    return localStorage.getItem(prf(name));
}

function getSetAsInt(name) {
    var c = getSet(name);
    return Number(c) ? c : 0;
}

function setSet(name, value) {
    return localStorage.setItem(prf(name), value);
}

function InitSession() {
    localStorage.removeItem(prf('SESSION_ID'));
    localStorage.removeItem(prf('SESSION_PRIVATE_KEY'));
    localStorage.removeItem(prf('SESSION_LAST_TICK_COUNT'));
    localStorage.removeItem(prf('SESSION_TICK_COUNT_OFFSET'));
    localStorage.removeItem(prf('SESSION_USERNAME'));
    return true;
}

function CloseSession() {
    if (!getSetAsInt('SESSION_ID')) return;

    $.ajax({
        type: "GET",
        dataType: "json",
        url: MAIN_URL + '/auth',
        data: {'session': getSetAsInt('SESSION_ID'), 'UserName': getSet('SESSION_USERNAME')},
        timeout: 2000,
        success: InitSession,
        error: InitSession
    });
}

// converted from TSQLRestClientURI.SessionSign function
function GetSessionSignature(url) {
    // expected format is 'session_signature='Hexa8(SessionID)+Hexa8(TimeStamp)+
    // Hexa8(crc32('SessionID+HexaSessionPrivateKey'+Sha256('salt'+PassWord)+
    // Hexa8(TimeStamp)+url))
    var d = new Date();
    var Tix = d.getTime();
    if (Tix < getSetAsInt('SESSION_LAST_TICK_COUNT')) // wrap around 0 after 49.7 days
        setSet('SESSION_TICK_COUNT_OFFSET', getSetAsInt('SESSION_TICK_COUNT_OFFSET') + 1 << (32 - 8)); // allows 35 years timing
    setSet('SESSION_LAST_TICK_COUNT', Tix);

    var Nonce = d2h(Tix >>> 8 + getSetAsInt('SESSION_TICK_COUNT_OFFSET'), 8);
    var sign = d2h(getSetAsInt('SESSION_ID'), 8) + Nonce + d2h(crc32(getSet('SESSION_PRIVATE_KEY') + Nonce + url), 8);
    var prf = '?';
    if (url.indexOf('?') > -1) prf = '&';
    return  prf + 'session_signature=' + sign;
}

$.ajaxPrefilter(function(options, _, jqXHR) {
    // signing all sended URLs
    if (getSetAsInt('SESSION_ID') > 0 && options.url.indexOf(MAIN_URL) > -1) { // if user authenticated
        var new_url = options.url;
        if (options.data && options.type == "GET")
        {
            new_url += '?' + options.data;
            options.data = null; // or options.data will be added to url by JQuery
        }
        options.url = new_url + GetSessionSignature(new_url.substr(SERVER_URL.length + 1));
        options.cache = true; // we don't want anti-cache "_" JQuery-parameter
    }
});

$(function() {
    if (typeof(localStorage) == 'undefined')
        alert('You do not have HTML5 localStorage support in your browser. Please update or application cannot work as expected');
});

I use HTML5 LocalStorage, not cookies, for storing session information.

And this is code for user authentication:
login.js:

function AuthorizeUser() {
    var name = $("#username").val();
    var pwd = $("#password").val();
    var servnonce = "";

    CloseSession(); // try close previously opened session

    var d = new Date();
    var clientnonce = d.getTime() / (1000 * 60 * 5); // valid for 5*60*1000 ms = 5 minutes;
    clientnonce = SHA256("" + clientnonce);

    var dataString = {'UserName': name};

    $.ajax({
        type: "GET",
        dataType: "json",
        url: MAIN_URL + '/auth',
        data: dataString,
        timeout:2000,
        beforeSend: function(jqXHR, settings) {
            $.blockUI({
                message: '<h1><img src="/images/busy.gif" style="height:16px;width:16px;" /> Авторизуюсь...</h1>',
                css: {
                    border: 'none',
                    padding: '15px',
                    backgroundColor: '#000',
                    '-webkit-border-radius': '10px',
                    '-moz-border-radius': '10px',
                    opacity: .5,
                    color: '#fff'
                } });
        },
        success: function(data, textStatus, jqXHR) {
            servnonce = data.result;
            // The Password parameter as sent for the 2nd request will be computed as
            // ! Sha256(ModelRoot+Nonce+ClientNonce+UserName+Sha256('salt'+PassWord))
            var password = SHA256(SERVER_ROOT + servnonce + clientnonce + name + SHA256('salt' + pwd));
            dataString = {'UserName': name, 'Password': password, 'ClientNonce': clientnonce};

            // second handshake
            $.ajax({
                type: "GET",
                dataType: "json",
                url: MAIN_URL + '/auth',
                data: dataString,
                timeout: 2000,

                success: function(data, textStatus, jqXHR) {
                    var p = data.result.indexOf('+');
                    if (p > -1) {
                        setSet('SESSION_ID', data.result.substr(0, p));
                        setSet('SESSION_PRIVATE_KEY', data.result + SHA256('salt' + pwd));
                        setSet('SESSION_USERNAME', name);

                        // get session info
                        $.ajax({
                            type: "GET",
                            dataType: "json",
                            url: MAIN_URL + '/GetSessionInfo',
                            timeout: 2000,
                            success: function(data) {
                                create_notify("msgs", { title:'Welcome', text:'You are successfully authenticated', icon:'check48.png' });
                                setSet('SESSION_GROUP', data.group);
                                setSet('SESSION_FIO', data.fio);
                                setTimeout(function() {
                                    document.location = '/pages/main_' + data.group + '.htm';
                                }, 1000);
                            }
                        });
                        $.unblockUI();

                        return true;
                    }
                },
                error: function (jqXHR, textStatus, errorThrown) {
                    if (jqXHR.status == 404) {
                        create_notify('msgs', {title:'Authentication error', text:'Sorry, wrong username or password', icon:'error48.png'});
                        $("#login section").effect("shake", 150);
                        $.unblockUI();
                        return
                    }
                }
            }); // end success of second handshake
        } // end success of first handshake
    });

    return false; // end of AuthorizeUser
}

$(function() {
    // Call the button widget method on the login button to format it.
    $("#btnLogin").button().bind("click", function() {
        AuthorizeUser();
        return false;
    });
});

Also i patch TSQLRestServer.Auth function (SQLite3Commons.pas):
Replace

...
       try
          User := nil; // will be freed by TAuthSession.Destroy
          aResp := Session.fPrivateSalt;
          if fSessions=nil then
...

with

...
        try
          User := nil; // will be freed by TAuthSession.Destroy
          aResp := JSONEncodeResult([Session.fPrivateSalt]);

          if fSessions=nil then
...

and

...
    begin
      // only UserName=... -> return hexadecimal nonce content valid for 5 minutes
      aResp := Nonce(false);
    end;
...

with

...
    begin
      // only UserName=... -> return hexadecimal nonce content valid for 5 minutes
      aResp := JSONEncodeResult([Nonce(false)]);
    end;
...

P.S. SHA256 and CRC32 routines getted from http://www.webtoolkit.info/
P.P.S. Replace string

if (typeof(crc) == "undefined") { crc = 0; }

to

crc = 0;

in webtoolkit.crc32.js or you have get wrong results from crc32 function. Or maybe code above (from ab) worked well, i don't check smile
P.P.P.S. Sorry for my English smile

#6 mORMot 1 » JavaScript client » 2011-10-27 11:42:07

RangerX
Replies: 1

Hello! Somebody tried to make the web-client for use with mORMot?
There is client-side code (just in .html file on local hard drive) for submit button on login form (uses jquery):

$(function() {  
  $("#signin_submit").click(function() { 
  var dataString = 'username=admin&password=synopse';

$.ajax({
  type: "GET",
  cache: false,
  dataType: "jsonp", 
  url: "http://localhost:888/r/auth",
  data: dataString,

  success: function(data, textStatus, jqXHR) {
    alert('success: ' + data + textStatus);
  },

  error: function(jqXHR, textStatus, errorThrown) {
    alert("status: " + textStatus);
  }  
});  
return false;
  });  
});

There is simply server-side code:

var
  fModel: TSQLModel;
  fDB: TSQLRestServerDB;
  fServer: TSQLite3HttpServer;

procedure TfmMainServer.FormCreate(Sender: TObject);
begin
  fModel := TSQLModel.Create([TSQLAuthGroup, TSQLAuthUser],'r');
  fDB := TSQLRestServerDB.Create(fModel, ChangeFileExt(paramstr(0),'.db3'), True);
  fDB.NoAJAXJSON := False;
  fDB.CreateMissingTables(0);
  fServer := TSQLite3HttpServer.Create('888',[fDB]);
  fServer.OnlyJSONRequests := False;
end;

When i clicked on "Submit", browser firing up a "error" event with statusText = "parseerror". This is because mORMot HTTP server responses with content-type: application/json, but actually server return plain text string with "server nonce".
If change dataType from "jsonp" to "script", success event firing, but data parameter is "undefined" and i can't get "server nonce" sad
My question is: how to get "server nonce" with cross-domain AJAX (JSONP)? Or small example with mORMot RESTful authentication using browser (javascript) smile

P.S. Sorry for my English

Board footer

Powered by FluxBB