-
-
Save drovani/df732f165d9735ad635707db1020d55d to your computer and use it in GitHub Desktop.
string secret = "[shopify-multipass-secret]"; | |
string store = "[shopify-store]"; | |
var json = System.Text.Json.JsonSerializer.Serialize(new { | |
email = "[customer-email]", | |
created_at = DateTime.Now.ToString("O"), | |
identifier = "[customer-uid]", | |
//remote_ip = "" | |
}); | |
var hash = System.Security.Cryptography.SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(secret)); | |
var encryptionKey = new ArraySegment<byte>(hash, 0, 16).ToArray(); | |
var signatureKey = new ArraySegment<byte>(hash, 16, 16).ToArray(); | |
var initvector = new byte[16]; | |
new System.Security.Cryptography.RNGCryptoServiceProvider().GetBytes(initvector); | |
byte[] cipherData = new Func<byte[], byte[], byte[]>((iv, key) => | |
{ | |
byte[] encrypted; | |
using (var aes = System.Security.Cryptography.Aes.Create()) | |
{ | |
aes.Key = encryptionKey; | |
aes.IV = iv; | |
aes.Mode = System.Security.Cryptography.CipherMode.CBC; | |
var encryptor = aes.CreateEncryptor(aes.Key, aes.IV); | |
using (MemoryStream ms = new MemoryStream()) | |
using (System.Security.Cryptography.CryptoStream cs = new System.Security.Cryptography.CryptoStream(ms, encryptor, System.Security.Cryptography.CryptoStreamMode.Write)) | |
{ | |
using (StreamWriter sw = new StreamWriter(cs)) | |
{ | |
sw.Write(json); | |
} | |
encrypted = ms.ToArray(); | |
} | |
} | |
return encrypted; | |
})(initvector, encryptionKey); | |
byte[] cipher = initvector.Concat(cipherData).ToArray(); | |
byte[] signature = new HMACSHA256(signatureKey).ComputeHash(cipher); | |
string token = Convert.ToBase64String(cipher.Concat(signature).ToArray()).Replace("+", "-").Replace("/", "_"); | |
string url = $"https://{store}.myshopify.com/account/login/multipass/{token}"; |
Good question, @flyinmryan.
The docs on RNGCryptoServiceProvider provide a bit of context for what's happening. The .GetBytes (byte[] data)
method "fills an array of bytes with a cryptographically strong sequence of random values." Line 15 could be split into two lines:
var provider = new System.Security.Cryptography.RNGCryptoServiceProvider();
provider.GetBytes(initvector); // fill initvector with random values.
Something of note is that RNGCryptoServiceProvider
is marked as obsolete in .NET 6, and (while I haven't tested it), I think you can replace lines 14 and 15 with something like this:
byte[] initvector = RandomNumberGenerator.Create().GetBytes(16);
Thanks for the quick response. I did check out the documentation shortly after I asked the question and it does make sense, but I guess it's the context of my implementation that is causing me headaches. I am using .NET Framework 4.7.2 to build out a custom Shopify store that is currently live and uses the Admin Api/GraphQL queries/Javascript BuySDK, and Shopify gateway for checkout. My task now is to implement Multipass. I have activated Multipass and followed the limited example provided in their docs, as well as your solution, but no success. I wrote code from scratch at first following the steps but I only get the one recurring error from Shopify that provides no insight to the problem. I've created the customer object programmatically using Newtonsoft to convert to Json, done the encryption as instructed, but again no luck. The forums offer countless similar questions asked but no solution, which is really hard to believe. Do you actually have Multipass working with seamless login/account creation in Shopify for non-Shopify users? I am beginning to wonder if it's even possible with .NET Framework at all. Thanks
@flyinmryan - the Shopfy Multipass errors are notoriously unhelpful and terrible to debug. It has nothing to do with .NET vs React vs any other framework. My first step to debugging has usually been to pipe the output (headers & payload) to some logs and make sure it is sending what is expected.
Last question, I promise. I have noticed a debate that's taken place on the internet between people that are smarter than I am, and even though I believe a consensus has been reached I don't know what that was. It's regarding what the garbage collector takes care of and what must be disposed of.
The SHA256 has a Create() function but is never disposed of:
var hash = System.Security.Cryptography.SHA256.Create().ComputeHash(Encoding.UTF8.GetBytes(secret));
whereas I see for the AES encryption you wrapped it in a using statement
using (var aes = System.Security.Cryptography.Aes.Create())
I take it you stand on the side of the GC handles it? I used some of your code here for multipass and someone commented about the SHA256 not being Dispose()'d.
Thanks!
Garbage collection is a complicated topic, akin to cache invalidation. There's no perfect solution, everyone has an opinion, and nothing works universally.
Having said that, unless you have large, long-running applications, chances are that you'll never really need to worry about the intricacies of GC methodologies. When an app closes, garbage collection will typically clear up everything you forgot to dispose.
Wrapping an IDisposable
in a using
statement is a good practice with little downside. Assuming the class implements IDisposable
according to best practice, the objects will all get disposed during the finalizer.
More about that can be found at CA1063: Implement IDisposable correctly.
Thanks again for taking the time to explain your code. It really boggles my mind how inadequate and full of traps Shopify’s API documentation and development experience are. There are no shortage of questions on their community forum, but without answers to many of them it’s counterproductive. Your code makes sense now, and had you not posted this I would have probably given up.
Thanks for the example, but what exactly is going on at line 15? You declare a new instance and then call GetBytes, but why are you getting bytes? They aren't being saved to a variable or affecting the initvector are they? I get the error that I must declare a return type.