Apps that have a server-side component generally need to identify the store through an authentication system. To avoid requiring users to log in again after already logging in to the ecommerce back office, Open2b passes the information needed to identify and authenticate the store to the app.
First of all, you need the store key. Apps published on the Store receive the key with the installation request on the store, while for other apps you can read it in the store back office under "Apps > Edit" by clicking the row corresponding to the app:

When an app is opened, its URL is requested with a GET and the "auth" parameter is passed, which contains all the information needed to authenticate the store:
GET /app.html?auth=SB7QMA2CYG.XoNxV5ITJVOztj8rReXC19ECnXQ9yElfWP0dE1Wwu8Q.eyJzaG9wIjoiMTIzNDU2Nzg5MCJ9
The auth parameter is made up of three parts separated by a dot. The first is the store identifier (in the example it is "SB7QMA2CYG"). The second and third parts are Base64url-encoded and are respectively a signature and a JSON object containing an "expires" field with a date and time in Unix format:
{
"expires" : "1372755729"
}
The signature and the "expires" field in the JSON object are used to verify that the request actually comes from the specified store and has not expired.
auth parameter: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 signatures
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
With the help of the previous function, we validate the request:
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")
// assuming the auth variable (String) contains the "auth" parameter of the request
String[] fields = auth.split("\\.", 3);
if ( ParseAuthRequest( shopKey( fields[0] ), fields[1], fields[2]) != null) {
// authorized
} else {
// not authorized
}
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
You only need to implement the function shop_keyshopKey which, given the store identifier, returns the key.
Normally, to authenticate calls following the app opening (that is, calls the JavaScript in your app makes to its server-side), you would use cookies: the server-side code sets a cookie in the browser, which is then sent back to the server with each call. However, because apps run inside an iframe, cookies may not work in some browsers.
If you are building an external app for a specific client, this may not be an issue if you know the browser the client uses and can access its settings. But if you want the app to work in every context or you want to publish the app on the Store, cookies cannot be used.
In this case the Admin SDK provides the JavaScript function Admin.getAuthRequest which returns an auth string like the one used to open the app. For each server call it is best to always call Admin.getAuthRequest to get a new auth (in practice, to optimize, Admin.getAuthRequest reuses the same auth for a certain time).
Returns the authentication string used by the server-side code to verify which store the request comes from.
In the following example the auth string is retrieved and passed to the server along with other app-specific parameters.
Admin.getAuthRequest(function(auth) {
var request = new XMLHttpRequest();
request.open('POST', 'https://www.myapp.com/?auth='+auth, true);
request.send(body);
});
Unlike the initial app opening, subsequent times the auth parameter can be passed to the server in any way you prefer, for example in the query string (as in the previous example), in the request body, or as an HTTP header. Authentication works the same way as when the app is opened.