Skip to content

Instantly share code, notes, and snippets.

@cigzigwon
Last active May 13, 2022 17:56
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cigzigwon/62867862541dfbb492025daa85ae13a6 to your computer and use it in GitHub Desktop.
Save cigzigwon/62867862541dfbb492025daa85ae13a6 to your computer and use it in GitHub Desktop.
Oauth 1.0a Specification Keyforger

Oauth Keyforge Test

Oauth Module

defmodule Oauth do
  # 6789973 Acct# for NetSuite Support
  @acct_id 6_789_973
  @base_uri "https://#{@acct_id}.suitetalk.api.netsuite.com/services/rest/record/v1/"
  @oauth_creds [
    realm: "#{@acct_id}",
    oauth_consumer_key: "1a1a4402b8a1dd876e67352e12b1a94ba6c5620f15d349f101b0059c13ecf030",
    oauth_consumer_secret: "949462be662ece983c8cf8a79e96c0901d2d8d2efd584f4c82ba9ab5c4ad3c8b",
    oauth_token: "df92b02bffec93f03c0f3454437cd5115efad796da37e90321f4ea02732d59e2",
    oauth_token_secret: "db60928d4387dabc626d23ce01fa5b68c19fd645871b35ba925608052d65262c",
    oauth_signature_method: "HMAC-SHA256",
    oauth_version: "1.0"
  ]

  def forge_oauth_head(method, url, qs \\ []) do
    timestamp = DateTime.utc_now() |> DateTime.to_unix() |> Integer.to_string()
    seed = :crypto.strong_rand_bytes(16) |> Base.encode16()
    nonce = :crypto.hash(:md5, seed) |> Base.encode16(case: :lower) |> String.slice(0..12)

    head =
      (@oauth_creds ++ [oauth_timestamp: timestamp, oauth_nonce: nonce])
      |> Enum.reject(fn {key, _} ->
        [:oauth_consumer_secret, :oauth_token_secret] |> Enum.member?(key)
      end)
      |> Enum.reduce([], fn {key, value}, acc ->
        acc ++ ["#{key}=\"#{value |> URI.encode_www_form()}\""]
      end)
      |> Enum.join(",")

    {"Authorization",
     "OAuth #{head}" <>
       ",oauth_signature=\"" <>
       forge_sign(method <> "&" <> (url |> URI.encode_www_form()), timestamp, nonce, qs) <> "\""}
  end

  def forge_sign(head, timestamp, nonce, qs) do
    params =
      (@oauth_creds ++ [oauth_timestamp: timestamp, oauth_nonce: nonce] ++ qs)
      |> Enum.reject(fn {key, _} ->
        [:realm, :oauth_consumer_secret, :oauth_token_secret] |> Enum.member?(key)
      end)
      |> Enum.sort_by(fn {key, val} ->
        {key, val}
      end)
      |> Enum.reduce([], fn {key, value}, acc ->
        acc ++ ["#{key}=#{value |> URI.encode(&URI.char_unreserved?(&1))}"]
      end)
      |> Enum.join("&")
      |> URI.encode_www_form()

    key =
      (@oauth_creds |> Keyword.fetch!(:oauth_consumer_secret)) <>
        "&" <> (@oauth_creds |> Keyword.fetch!(:oauth_token_secret))

    :crypto.mac(:hmac, :sha256, key, head <> "&" <> params)
    |> Base.encode64(case: :lower)
    |> URI.encode_www_form()
  end
end
Oauth.forge_oauth_head("GET", "https://pyroclasti.cloud", username: "cigzigwon")
ExUnit.start()
defmodule OauthTest do
  use ExUnit.Case

  @acct_id 6_789_973
  @customer_uri "https://#{@acct_id}.suitetalk.api.netsuite.com/services/rest/record/v1/customer"

  test "forge_sign/4 can return an oauth_signature using required header params" do
    timestamp = DateTime.utc_now() |> DateTime.to_unix() |> Integer.to_string()
    seed = :crypto.strong_rand_bytes(16) |> Base.encode16()
    nonce = :crypto.hash(:md5, seed) |> Base.encode16(case: :lower) |> String.slice(0..12)

    oauth_signature =
      Oauth.forge_sign(
        "POST&#{@customer_uri |> URI.encode_www_form()}",
        timestamp,
        nonce,
        []
      )

    assert oauth_signature |> String.length() >= 46
  end

  test "forge_sign/4 can return an oauth_signature using qs params" do
    timestamp = DateTime.utc_now() |> DateTime.to_unix() |> Integer.to_string()
    seed = :crypto.strong_rand_bytes(16) |> Base.encode16()
    nonce = :crypto.hash(:md5, seed) |> Base.encode16(case: :lower) |> String.slice(0..12)

    oauth_signature =
      Oauth.forge_sign(
        "POST&#{@customer_uri |> URI.encode_www_form()}",
        timestamp,
        nonce,
        username: "cigzigwon"
      )

    assert oauth_signature |> String.length() >= 46
  end

  test "forge_oauth_head/3 can return an Authorization tuple header for OAuth 1.0a" do
    {"Authorization", value} = Oauth.forge_oauth_head("POST", @customer_uri)
    assert value |> String.contains?("oauth_consumer_secret") == false
    assert value |> String.contains?("oauth_token_secret") == false
  end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment