Current section
Files
Jump to
Current section
Files
lib/shopify/multipass.ex
defmodule Shopify.Multipass do
@moduledoc """
Logins can be accomplished by enabling the Multipass feature on your Shopify Plus account.
This requires the use of a special Multipass session token.
[https://help.shopify.com/en/api/reference/plus/multipass](https://help.shopify.com/en/api/reference/plus/multipass)
"""
# The size of the crypto used by http://erlang.org/doc/man/crypto.html#block_encrypt-4
@block_size 16
@doc ~S"""
Simple getter to expose the module attribute
"""
def get_block_size(), do: @block_size
@doc ~S"""
Get the full URL for the Multipass signin.
"""
@spec get_url(binary, map, binary, map) :: binary
def get_url(shop_name, customer_data, multipass_secret, config_opts \\ %{}) do
config = Shopify.Config.new(config_opts)
%URI{
host: "#{shop_name}.#{config.host}",
path: "/account/login/multipass/#{get_token(customer_data, multipass_secret, config_opts)}",
port: config.port,
scheme: config.scheme
}
|> URI.to_string()
end
@doc ~S"""
Calculates the Shopify Multipass token: an encrypted and signed message containing the customer data to be used for
the Multipass SSO.
"""
@spec get_token(map, binary, map) :: binary
def get_token(customer_data, multipass_secret, config_opts \\ %{}) do
config = Shopify.Config.new(config_opts)
{encryption_key, signature_key} = get_keys(multipass_secret)
config.json_codec.encode!(customer_data)
|> encrypt(encryption_key)
|> sign(signature_key)
|> Base.url_encode64(case: :lower)
end
@doc ~S"""
Pads the message string with extra bytes to ensure it is evenly divisible by
the block size.
See [http://erlang.org/doc/man/crypto.html#block_encrypt-4](http://erlang.org/doc/man/crypto.html#block_encrypt-4)
"""
@spec pad(binary, integer) :: binary
def pad(string, block_size \\ @block_size) do
to_add = block_size - rem(byte_size(string), block_size)
string <> :binary.copy(<<to_add>>, to_add)
end
@doc ~S"""
Encrypts a message using the given encryption key. This will pad the message according to the block size.
Returns the encrypted message as cipher text.
"""
@spec encrypt(binary, binary, integer) :: binary
def encrypt(message, encryption_key, block_size \\ @block_size) do
initialization_vector = :crypto.strong_rand_bytes(block_size)
initialization_vector <>
:crypto.crypto_one_time(
:aes_128_cbc,
encryption_key,
initialization_vector,
pad(message, block_size),
true
)
end
@doc ~S"""
Signs the given cipher text message using the provided signature key
"""
@spec sign(binary, binary) :: binary
def sign(cipher_text, signature_key) do
signature = :crypto.mac(:hmac, :sha256, signature_key, cipher_text)
cipher_text <> signature
end
@doc ~S"""
Splits the multipass secret into 2 binaries, each containing exactly the block size number of bytes.
Returns a tuple where the first item is the encryption_key, the second is the signature_key
"""
@spec get_keys(binary, integer) :: tuple
def get_keys(multipass_secret, block_size \\ @block_size) do
key_material = :crypto.hash(:sha256, multipass_secret)
# Split the key into 2 binaries each containing exactly 16 bytes
<<encryption_key::binary-size(block_size), signature_key::binary-size(block_size)>> =
key_material
{encryption_key, signature_key}
end
end