Autenticazione

Le app che hanno una parte lato server hanno in genere necessità di riconoscere il negozio attraverso un sistema di autenticazione. Per evitare che gli utenti debbano fare nuovamente il login, in quanto già fatto per accedere al gestionale ecommerce, Open2b Commerce Ready passa all'app le informazioni necessarie per riconoscere e autenticare il negozio.

Bisogna prima di tutto avere la chiave del negozio. Le app pubblicate sullo Store ricevono la chiave con la richiesta di installazione sul negozio mentre per le altre app è possibile leggerla nel gestionale del negozio in "Apps > Modifica" e facendo clic sulla riga corrispondente all'app:

Quando viene aperta una app, viene richiamato con una GET il suo indirizzo e viene passato il parametro "auth" che contiene tutte le informazioni necessarie per autenticare il negozio:

GET /app.html?auth=SB7QMA2CYG.XoNxV5ITJVOztj8rReXC19ECnXQ9yElfWP0dE1Wwu8Q.eyJzaG9wIjoiMTIzNDU2Nzg5MCJ9

Il parametro auth è composto da tre parti separate da un punto. La prima è l'identificativo del negozio ( nell'esempio è "SB7QMA2CYG" ). La seconda e terza parte sono codificate in formato Base64url e sono rispettivamente una firma e un oggetto JSON contenente un campo "expires" con una data e ora in formato unix:

    {
       "expires" : "1372755729"
    }

La firma e il campo "expires" dell'oggetto JSON servono per verificare che la richiesta provenga effettivamente dal negozio indicato e non sia scaduta.

Esempio completo in

La seguente funzione parsa e valida il parametro auth:
require Digest::SHA;
require JSON;
require MIME::Base64;
require Time::Local;

sub parse_auth_request {
    my ($key, $signature, $data) = @_;
  
    my $decoded_key       = MIME::Base64::decode_base64url($key);
    my $decoded_signature = MIME::Base64::decode_base64url($signature);
  
    return if $decoded_signature ne Digest::SHA::hmac_sha256($data, $decoded_key);
 
    my $request = JSON::decode_json(MIME::Base64::decode_base64url($data));
    return if $request->{"expires"} < Time::Local::timegm(gmtime());

    return $request;
}
function parse_auth_request($key, $sign, $data) {
  $decoded_key  = base64_decode(strtr($key, '-_', '+/'));
  $decoded_sign = base64_decode(strtr($sign, '-_', '+/'));
  if ( $decoded_sign !== hash_hmac('sha256', $data, $decoded_key, true) ) {
      return null;
  }
  $request = json_decode(base64_decode(strtr($data, '-_', '+/')), true);
  if ( $request['expires'] < time() ) {
      return null;
  }
  return $request;
}
    
import base64
import hashlib
import hmac
import json
import time
import calendar
def parse_auth_request(key, signature, data):

    decoded_key       = base64.urlsafe_b64decode((key+ "=" *((4 - len(key) % 4)%4)).encode("ascii"))
    decoded_signature = base64.urlsafe_b64decode((signature+ "=" *((4 - len(signature) % 4)%4)).encode("ascii"))
  
    if decoded_signature != hmac.new(decoded_key,msg=data.encode("ascii"),digestmod=hashlib.sha256).digest():
        return None
  
    request = json.loads(base64.urlsafe_b64decode((data+ "=" *((4 - len(data) % 4)%4)).encode("ascii")).decode('ascii'))
  
    if request["expires"] < calendar.timegm(time.gmtime()):
        return None
    return request
import org.codehaus.jackson.map.ObjectMapper;
import org.apache.commons.codec.binary.Base64;
import java.security.MessageDigest;
import java.util.Map;
import java.util.Arrays;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public static Map<?,?> ParseAuthRequest(String key, String signature, String data) {

    // decode the key and the signature
    Base64 b64Decoder       = new Base64(true);
    byte[] decodedKey       = b64Decoder.decodeBase64(key);
    byte[] decodedSignature = b64Decoder.decodeBase64(signature);
    
    // sign the message
    SecretKeySpec signingKey = new SecretKeySpec(decodedKey, "HmacSHA256");
    Mac mac;
    try {
        mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
    } catch(Exception e) {
        return null;
    }
    
    byte[] messageSignature = mac.doFinal(data.getBytes());
    
    // compare the segnatures
    if (!Arrays.equals(messageSignature, decodedSignature)) {
        return null;
    }
    
    // decode the data
    ObjectMapper mapper = new ObjectMapper();
    Map<?,?> request;
    long expireTime; 
    try {
        request = mapper.readValue(new String(b64Decoder.decodeBase64(data)), Map.class);
        expireTime= Long.parseLong(request.get("expires").toString());
    } catch(Exception e) {
        return null;
    }
    
    //check if the request is expired
    if (expireTime < (System.currentTimeMillis() / 1000)) {
        return null;
    }
    
    return request;
}
require 'base64'
require 'openssl'
require 'json'
require 'time'

def parse_auth_request(key, signature, data)
  
  decoded_key       = Base64.urlsafe_decode64(key + "=" * ((4 - key.length % 4) % 4))
  decoded_signature = Base64.urlsafe_decode64(signature + "=" * ((4 - signature.length % 4) % 4))
  
  if decoded_signature != OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, decoded_key, data)
    return nil
  end
  
  request = JSON.parse(Base64.urlsafe_decode64(data + "=" * ((4 - data.length % 4) % 4)))
  
  if Time.at(request["expires"]).getutc() < Time.new.getutc()
    return nil
  end
  
  return request

end

Con l'aiuto della precedente funzione validiamo la richiesta:

require CGI;

my $cgi = CGI->new();

my $auth               = $cgi->param("auth");
my ($shop,$sign,$data) = $auth=~/^(.*)\.(.*)\.(.*)$/;

my $request = parse_auth_request(shop_key($shop),$sign,$data);
if (!defined $request) {
    print $cgi->header(-status=>"403 Forbidden", -type=>"text/plain");
    print "You are not allowed to access this app.";
    return;
}
list($shop, $sign, $data) = explode('.', $_GET['auth'], 3);
if ( parse_auth_request(shop_key($shop), $sign, $data) == null ) {
    header('HTTP/1.0 403 Forbidden');
    echo '<h1>403 Forbidden</h1>';
    echo 'You are not allowed to access this app.';
    die;
}
import cgi
import cgitb

form = cgi.FieldStorage()
auth = form.getvalue("auth")
shop, signature, data = map(str, auth.split('.', 2))
if parse_auth_request(shop_key(shop), signature, data) == None:
    print("Status:403 Forbidden")
    print("Content-type:text/plain\r\n\r\n")
    print("Not authorized")
// supponendo che variabile auth (di tipo String) contenga il parametro "auth" della richiesta
String[] fields = auth.split("\\.", 3);
if ( ParseAuthRequest( shopKey( fields[0] ), fields[1], fields[2]) != null) {
    // autorizzato
} else {
    // non autorizzato
}
require 'cgi'
cgi = CGI.new
shop, signature, data = cgi["auth"].split(".")
if parse_auth_request(shop_key(shop), signature, data) == nil
  puts cgi.header({
    "status" => "403 \"Forbidden",
    "type"   => "text/html",
  })
  puts "<html><body>"
  puts "access denied"
  puts "</body></html>"
  abort
end

Vi resta solamente da implementare la funzione shop_keyshopKey che dato l'identificativo nel negozio ne ritorna la chiave.

Autenticare le chiamate successive

In genere per autenticare le chiamate successive all'apertura dell'app, ossia le chiamate che il JavaScript della vostra app esegue verso il suo lato server, si utilizzerebbero i cookie: il codice lato server mette un cookie nel browser che poi questo reinvierà indietro al server con ogni chiamata. Siccome però le app vengono eseguite all'interno di un iframe, con alcuni browser i cookie potrebbero non funzionare.

Se si sta realizzando un'app esterna per un determinato cliente, questo potrebbe non essere un problema se si conosce il browser utilizzato dal cliente e si può accedere alle sue impostazioni. Ma se si vuole che l'app funzioni in ogni contesto o si vuole pubblicare l'app sullo Store allora i cookie non si possono utilizzare.

In questo caso il CR SDK mette a disposizione del JavaScript dell'app la funzione CR.getAuthRequest che ritorna una stringa auth come quella utilizzata per l'apertura dell'app. Per ogni chiamata al server è bene chiamare sempre CR.getAuthRequest per avere un nuovo auth ( in realtà per ottimizzare la CR.getAuthRequest riutilizza lo stesso auth per un certo tempo ).

CR.getAuthRequest(function(auth) { … })

Ritorna la stringa di autenticazione utilizzata dal codice lato server per verificare da quale negozio proviene la richiesta.

Esempio:

Nel seguente esempio viene recuperata la stringa auth e viene passata al server assieme ad altri parametri propri dell'app.

CR.getAuthRequest(function(auth) {
    var request = new XMLHttpRequest();
    request.open('POST', 'https://www.myapp.com/?auth='+auth, true);
    request.send(body);
});

Diversamente dall'apertura iniziale dell'app, le volte successive il parametro auth può essere passato al lato server come si preferisce, ad esempio nel query string (come nel precedente esempio), nel corpo della richiesta o come intestazione HTTP. L'autenticazione avviene allo stesso modo come nel caso dell'apertura dell'app.