Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add the OAuth verification and header #27

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 6 additions & 93 deletions lib/lti.ex
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule LTI do
@moduledoc """
A library to launch a LTI request
"""
alias LTI.{Credentials, OAuthData, LaunchParams}
alias LTI.{Credentials, Helpers, OAuthData, LaunchParams}

def credentials(url, key, secret) do
%Credentials{url: url, key: key, secret: secret}
Expand All @@ -13,109 +13,22 @@ defmodule LTI do
oauth_callback: "about:blank",
oauth_consumer_key: key,
oauth_version: "1.0",
oauth_nonce: nonce(),
oauth_timestamp: timestamp(),
oauth_nonce: Helpers.nonce(),
oauth_timestamp: Helpers.timestamp(),
oauth_signature_method: "HMAC-SHA1"
}
end

def launch_data(%OAuthData{} = oauth, %LaunchParams{} = launch_params) do
struct_to_list(launch_params) ++ struct_to_list(oauth)
end

def signature(
%Credentials{secret: secret} = creds,
%Credentials{secret: secret, url: url},
%OAuthData{} = oauth_params,
%LaunchParams{} = launch_params
) do
:sha
|> :crypto.hmac(
encode_secret(secret),
base_string(creds, oauth_params, launch_params)
Helpers.encode_secret(secret),
Helpers.base_string(url, oauth_params, launch_params)
)
|> Base.encode64()
end

defp encode_secret(secret) do
"#{percent_encode(secret)}&"
end

defp base_string(%Credentials{url: url}, oauth_params, launch_params) do
{normalized_url, query} = parse_url(url)
query_params = to_query_params(query)

query =
oauth_params
|> launch_query(launch_params, query_params)
|> Enum.join("&")
|> percent_encode()

"POST&#{percent_encode(normalized_url)}&#{query}"
end

def launch_query(%OAuthData{} = oauth, %LaunchParams{} = launch_params, query_string_params) do
parameters = launch_data(oauth, launch_params) ++ query_string_params

parameters
|> Enum.reduce(%{}, fn {key, value}, acc ->
Map.put(acc, key, value)
end)
|> Enum.reduce([], fn {key, value}, acc ->
acc ++ ["#{key}=#{percent_encode(value)}"]
end)
end

defp parse_url(url) do
%URI{scheme: scheme, authority: authority, path: path, query: query} = URI.parse(url)
normalized_url = "#{scheme}://#{authority}#{path}"
{normalized_url, query}
end

defp to_query_params(nil), do: []

defp to_query_params(query) do
query
|> String.split("&")
|> Enum.map(fn pair ->
[key, value] = String.split(pair, "=")
{String.to_atom(key), value}
end)
|> Keyword.new()
end

defp timestamp do
{megasec, sec, _mcs} = :os.timestamp()
"#{megasec * 1_000_000 + sec}"
end

defp percent_encode({key, value}) do
{percent_encode(key), percent_encode(value)}
end

defp percent_encode(other) do
other
|> to_string()
|> URI.encode(&URI.char_unreserved?/1)
end

defp struct_to_list(struct),
do:
struct
|> Map.from_struct()
|> Map.to_list()
|> strip_nil()

defp strip_nil(list) do
Enum.reduce(list, [], fn {_, value} = item, acc ->
if is_nil(value),
do: acc,
else: acc ++ [item]
end)
end

defp nonce do
24
|> :crypto.strong_rand_bytes()
|> Base.encode64()
end
end
102 changes: 102 additions & 0 deletions lib/lti/helpers.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
defmodule LTI.Helpers do
@moduledoc """
A module for helper functions that are used for the OAuth params
"""

alias LTI.{Credentials, LaunchParams, OAuthData}

def base_string(url, %OAuthData{} = oauth_params, %LaunchParams{} = launch_params) do
params = launch_data(oauth_params, launch_params)
base_string(url, params)
end

def base_string(url, oauth_params, launch_params) do
params = oauth_params ++ launch_params
base_string(url, params)
end

def percent_encode({key, value}) do
{percent_encode(key), percent_encode(value)}
end

def percent_encode(other) do
other
|> to_string()
|> URI.encode(&URI.char_unreserved?/1)
end

def nonce do
24
|> :crypto.strong_rand_bytes()
|> Base.encode64()
end

def timestamp do
{megasec, sec, _mcs} = :os.timestamp()
"#{megasec * 1_000_000 + sec}"
end

def encode_secret(secret) do
"#{percent_encode(secret)}&"
end

defp base_string(url, params) do
{normalized_url, query} = parse_url(url)
query_params = to_query_params(query)

query =
(params ++ query_params)
|> launch_query()
|> Enum.join("&")
|> percent_encode()

"POST&#{percent_encode(normalized_url)}&#{query}"
end

defp launch_query(parameters) do
parameters
|> Enum.reduce(%{}, fn {key, value}, acc ->
Map.put(acc, key, value)
end)
|> Enum.reduce([], fn {key, value}, acc ->
acc ++ ["#{key}=#{percent_encode(value)}"]
end)
end

defp to_query_params(nil), do: []

defp to_query_params(query) do
query
|> String.split("&")
|> Enum.map(fn pair ->
[key, value] = String.split(pair, "=")
{String.to_atom(key), value}
end)
|> Keyword.new()
end

defp launch_data(%OAuthData{} = oauth, %LaunchParams{} = launch_params) do
struct_to_list(launch_params) ++ struct_to_list(oauth)
end

defp parse_url(url) do
%URI{scheme: scheme, authority: authority, path: path, query: query} = URI.parse(url)
normalized_url = "#{scheme}://#{authority}#{path}"
{normalized_url, query}
end

defp struct_to_list(struct),
do:
struct
|> Map.from_struct()
|> Map.to_list()
|> strip_nil()

defp strip_nil(list) do
Enum.reduce(list, [], fn {_, value} = item, acc ->
if is_nil(value),
do: acc,
else: acc ++ [item]
end)
end
end
49 changes: 49 additions & 0 deletions lib/lti/oauth.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
defmodule LTI.OAuth do
@moduledoc """
Module containing functions to determine and compare oauth 1.0 signatures.
"""
alias LTI.{Credentials, Helpers}

def oauth_verification(secret, url, oauth_params, regular_params, signature) do
with basestring <- Helpers.base_string(url, oauth_params, regular_params),
{:ok, calculated_signature} <- signature(secret, basestring) do
if calculated_signature == signature do
{:ok, :oauth_successful}
else
{:error, :signatures_not_matching}
end
end
end

def generate_oauth_header(lis_outcome_service_url, %Credentials{key: key, secret: secret}) do
nonce = Helpers.nonce()
timestamp = Helpers.timestamp()

oauth_params = [
oauth_consumer_key: key,
oauth_nonce: nonce,
oauth_signature_method: "HMAC-SHA1",
oauth_version: "1.0",
oauth_timestamp: timestamp
]

basestring = Helpers.base_string(lis_outcome_service_url, oauth_params, [])
{:ok, calculated_signature} = signature(secret, basestring)

~s(OAuth oauth_consumer_key="#{key}", oauth_nonce="#{Helpers.percent_encode(nonce)}", oauth_signature_method="HMAC-SHA1", oauth_version="1.0", oauth_timestamp="#{
timestamp
}", oauth_signature="#{Helpers.percent_encode(calculated_signature)}")
end

defp signature(secret, basestring) do
signature =
:sha
|> :crypto.hmac(
Helpers.encode_secret(secret),
basestring
)
|> Base.encode64()

{:ok, signature}
end
end
3 changes: 2 additions & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ defmodule LTI.Mixfile do
defp deps do
[
{:credo, "~> 0.8", only: [:dev, :test], runtime: false},
{:ex_doc, ">= 0.0.0", only: [:dev, :test]}
{:ex_doc, ">= 0.0.0", only: [:dev, :test]},
{:mock, "~> 0.3.0", only: :test}
]
end
end
12 changes: 8 additions & 4 deletions mix.lock
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
%{"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [], [], "hexpm"},
"credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"}}
%{
"bunt": {:hex, :bunt, "0.2.0", "951c6e801e8b1d2cbe58ebbd3e616a869061ddadcc4863d0a2182541acae9a38", [:mix], [], "hexpm"},
"credo": {:hex, :credo, "0.8.10", "261862bb7363247762e1063713bb85df2bbd84af8d8610d1272cd9c1943bba63", [:mix], [{:bunt, "~> 0.2.0", [hex: :bunt, repo: "hexpm", optional: false]}], "hexpm"},
"earmark": {:hex, :earmark, "1.2.4", "99b637c62a4d65a20a9fb674b8cffb8baa771c04605a80c911c4418c69b75439", [:mix], [], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.18.1", "37c69d2ef62f24928c1f4fdc7c724ea04aecfdf500c4329185f8e3649c915baf", [:mix], [{:earmark, "~> 1.1", [hex: :earmark, repo: "hexpm", optional: false]}], "hexpm"},
"meck": {:hex, :meck, "0.8.13", "ffedb39f99b0b99703b8601c6f17c7f76313ee12de6b646e671e3188401f7866", [:rebar3], [], "hexpm"},
"mock": {:hex, :mock, "0.3.2", "e98e998fd76c191c7e1a9557c8617912c53df3d4a6132f561eb762b699ef59fa", [:mix], [{:meck, "~> 0.8.8", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm"},
}
77 changes: 77 additions & 0 deletions test/helpers_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
defmodule HelpersTest do
use ExUnit.Case

alias LTI.{Helpers, LaunchParams, OAuthData}

describe "#base_string/3" do
test "returns the base string with given params as arrays" do
oauth_params = [
oauth_consumer_key: "key",
oauth_nonce: "nonce"
]

basestring = Helpers.base_string("http://example.com", oauth_params, [])

assert basestring ==
"POST&http%3A%2F%2Fexample.com&oauth_consumer_key%3Dkey%26oauth_nonce%3Dnonce"
end

test "returns a base string with OAuthData and LaunchParam structs" do
oauth_credentials = %OAuthData{
oauth_callback: "about:blank",
oauth_consumer_key: "key",
oauth_version: "1.0",
oauth_nonce: "nonce",
oauth_timestamp: "timestamp",
oauth_signature_method: "HMAC-SHA1"
}

valid_launch_params = %LaunchParams{
context_id: "456434513",
launch_presentation_locale: "en",
launch_presentation_return_url: "url",
lis_person_contact_email_primary: "[email protected]",
lis_person_name_full: "whoot at waaht",
lti_message_type: "basic-lti-launch-request",
lti_version: "LTI-1p0",
resource_link_description: "A weekly blog.",
resource_link_id: "120988f929-274612",
resource_link_title: "onno schuit",
roles: "Student",
submit: "Launch",
tool_consumer_instance_guid: "lmsng.school.edu",
user_id: 1234
}

basestring =
Helpers.base_string("http://example.com", oauth_credentials, valid_launch_params)

assert basestring ==
"POST&http%3A%2F%2Fexample.com&context_id%3D456434513%26launch_presentation_locale%3Den%26launch_presentation_return_url%3Durl%26lis_person_contact_email_primary%3Duser%2540wtf.nl%26lis_person_name_full%3Dwhoot%2520at%2520waaht%26lti_message_type%3Dbasic-lti-launch-request%26lti_version%3DLTI-1p0%26oauth_callback%3Dabout%253Ablank%26oauth_consumer_key%3Dkey%26oauth_nonce%3Dnonce%26oauth_signature_method%3DHMAC-SHA1%26oauth_timestamp%3Dtimestamp%26oauth_version%3D1.0%26resource_link_description%3DA%2520weekly%2520blog.%26resource_link_id%3D120988f929-274612%26resource_link_title%3Donno%2520schuit%26roles%3DStudent%26submit%3DLaunch%26tool_consumer_instance_guid%3Dlmsng.school.edu%26user_id%3D1234"
end

test "returns an error when not supported" do
assert_raise ArgumentError, fn ->
Helpers.base_string("some_url", %{key: "value"}, [])
end
end
end

describe "#percent_encode/1" do
test "returns a tuple encoded" do
encoded_value = Helpers.percent_encode({:user_email, "[email protected]"})

assert encoded_value == {"user_email", "user%40wtf.nl"}
end

test "returns an encoded value" do
assert Helpers.percent_encode("[email protected]") == "user%40wtf.nl"
end
end

describe "#encode_secret/1" do
test "returns secret encoded" do
assert Helpers.encode_secret("secret") == "secret&"
end
end
end
Loading