fireproofsocks

fireproofsocks

I’m working on an Elixir implementation of Shopify’s Multipass feature – it’s basically a single-sign-on (SSO) flow that lets your app handle authentication, and on success, it produces a URL that when clicked will cause a customer record to be upserted into Shopify. They include working examples of how to do this in PHP and Ruby, but I cannot make it work in Elixir.

You have to enable the feature and they give you a 32 character shared secret key.

# Example key from Shopify Admin
multipass_secret = "1234567890abcdef1234567890abcdef"

customer_data = %{
  email: "test@test.shopify.com",
  created_at: DateTime.to_iso8601(Timex.now()),  # <-- the token will only be valid for a small timeframe around this timestamp.
}

key_material = :crypto.hash(:sha256, multipass_secret)
# Split the key into 2 binaries each containing exactly 16 bytes
<< encryption_key::binary-size(16), signature_key::binary-size(16) >> = key_material

customer_data_as_string = Jason.encode!(customer_data)

# Initialization Vector
ivec = :crypto.strong_rand_bytes(16)

to_add = 16 - rem(byte_size(customer_data_as_string), 16)
padded = customer_data_as_string <> :binary.copy(<<to_add>>, to_add)

cipher_text = ivec <> :crypto.block_encrypt(:aes_cbc128, encryption_key, ivec, padded)
signature = :crypto.hmac(:sha256, signature_key, cipher_text)
message = cipher_text <> signature

token = Base.encode16(message, case: :lower)

"https://yourstore.myshopify.com/account/login/multipass/#{token}"

Note that this feature is only available in Shopify Plus accounts.

Links I’ve been pouring over:

Does anyone have some pointers on converting this? Thanks!

Showing Posts 1 to 10

Ninigi

Ninigi

Do you get a meaningful error? What exactly happens when you try this?

fireproofsocks

fireproofsocks OP

No error on generation, but when I click the link to head over to Shopify, I get a bad request error. Using the PHP or Ruby code samples results in a working link that logs me into the Shopify store.

Ninigi

Ninigi

Before diving too deep into this, can you try

token = Base.url_encode64(message)

EDIT

I took the provided ruby code on the Shopify documentation, replaced dynamic values with static values (the json string and iv) and used the resulting token to set up a simple test.

I think you already had it:

multipass_secret = "1234567890abcdef1234567890abcdef"

customer_data = %{
  email: "test@test.shopify.com",
  created_at: "2019-03-07T11:34:51.153+09:00"
}

key_material = :crypto.hash(:sha256, multipass_secret)
# Split the key into 2 binaries each containing exactly 16 bytes
<< encryption_key::binary-size(16), signature_key::binary-size(16) >> = key_material

customer_data_as_string = Jason.encode!(customer_data)

# Initialization Vector
ivec = "testieivphrasefo" # :crypto.strong_rand_bytes(16)

to_add = 16 - rem(byte_size(customer_data_as_string), 16)
padded = customer_data_as_string <> :binary.copy(<<to_add>>, to_add)

cipher_text = ivec <> :crypto.block_encrypt(:aes_cbc128, encryption_key, ivec, padded)

signature = :crypto.hmac(:sha256, signature_key, cipher_text)

message = cipher_text <> signature

Base.url_encode64(message)
# Should give you:
# "dGVzdGllaXZwaHJhc2VmbxNXNKV-epmKd9gZPxrsFbqUbYcYVFjtPSleoB-61FpmhCtibtr8cxudaPBvk8QBqdggqsOofjr21PdwM4qvHNNJh8jyMzH9emUfjxNLj2M6dZ5BGyBlFhF7OWQHWljxQywhqP7RCC0klKG_zYwlNm8="

Just change Base.encode16/2 to Base.url_encode64/1

fireproofsocks

fireproofsocks OP

WOW. That has to be the best typo I’ve made in weeks! Base 16?!? I was so focused on the hard parts of the crypto stuff that I didn’t pay attention to the last bit. THANK YOU!!

fireproofsocks

fireproofsocks OP

For the record, here is the complete working proof-of-concept code:

# Use your multipass secret from the Shopify Dashboard: Settings -> Checkout 
multipass_secret = "1234567890abcdef1234567890abcdef"
block_size = 16

customer_data = %{
  email: "test@test.shopify.com",
  created_at: DateTime.to_iso8601(Timex.now()), # Must be a current time
}

# Split the secret into 2 binary keys each containing exactly 16 bytes
key_material = :crypto.hash(:sha256, multipass_secret)
<< encryption_key::binary-size(16), signature_key::binary-size(16) >> = key_material

# Encode the message payload
customer_data_as_string = Jason.encode!(customer_data)

# Initialization Vector
ivec = :crypto.strong_rand_bytes(block_size)

# Padding
to_add = block_size - rem(byte_size(customer_data_as_string), block_size)
padded = customer_data_as_string <> :binary.copy(<<to_add>>, to_add)

# Manually pad the message with the IV
cipher_text = ivec <> :crypto.block_encrypt(:aes_cbc128, encryption_key, ivec, padded)
signature = :crypto.hmac(:sha256, signature_key, cipher_text)
message = cipher_text <> signature

token = Base.url_encode64(message, case: :lower)

# The magic multipass link to your site:
"https://yoursite.myshopify.com/account/login/multipass/#{token}"
maz

maz

It’s so easy to mess-up crypto.

NobbZ

NobbZ

Yeah. That might be the reason behind the first rule of the crypto club:

Don’t do crypto (on your own)

Ninigi

Ninigi

Good to hear you solved it :+1:

If you have some spare time, a pull request to implement multipass would be more than welcome!

fireproofsocks

fireproofsocks OP

Sure – the process does require that customer data be JSON encoded. Can you recommend how to nicely implement that as a dependency? Or should I restructure the input to make the user do the JSON encoding outside of your package?

Ninigi

Ninigi

the package already uses Poison for json processing, so I think you should not have to introduce a new dependency :slight_smile:

Where Next? Top

Trending in Questions Top

katta
I having some trouble figuring out if I have set myself too strict of standards for my production server. Currently I can handle 75% of r...
New
achenet
Hello, I’m trying to build a basic Phoenix web-app, and I’d like to use Tailwind. However, when I launch mix phx.server, I get an error...
New
Cxx-mlr
I’m working on a small exercise involving update_in/3, and I came up with this solution: data = %{ name: "Periodic Table", category:...
New
ChrisAmelia
I’ve got trouble wrapping my head around the order in which functions are called in this snippet (from Phoenix’s authentication): toke...
New
dillonoconnor
Is there any way to avoid the Hologram compiler running when using iex? It seems like the front-end code could potentially be disregarded...
New
thiagogsr
** (ArgumentError) expected :max_attempts to be a positive integer, got: {:@, [line: 10, column: 19], [{:max_attempts, [line: 10, column:...
New
unaware8150
Hello folks! So at work, we are seeing some situations where we have to define some “fixed” strings that are used across the codebase in...
New

Other Trending Topics Top

GenericJam
Edit: 2026 May 15 - This post is archived. Mob is alive!! Main docs: mob v0.7.11 — Documentation A bit of explanation for the slightly c...
New
mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
budgie
A little off-topic, but I feel like people here have a good head on their shoulders. I used to be quite good at making software. Was luc...
New
KristerV
Hey. Is there anyone here who creates agents in their apps? Not talking about using agents, but creating them. I’m finding it pretty diff...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews