Skip to content

Commit

Permalink
Merge pull request #6 from Boulevard/lnikkila/token-structs
Browse files Browse the repository at this point in the history
Return and consume tokens as structs
  • Loading branch information
lnikkila authored Mar 9, 2017
2 parents de41062 + 8e9b1ad commit 7ff3329
Show file tree
Hide file tree
Showing 7 changed files with 117 additions and 104 deletions.
2 changes: 1 addition & 1 deletion lib/exquickbooks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ defmodule ExQuickBooks do
# Returns the configured OAuth credentials.
@doc false
def credentials do
for k <- @credential_config do
for k <- @credential_config, into: %{} do
case get_env(k) do
v when is_binary(v) ->
{k, v}
Expand Down
30 changes: 10 additions & 20 deletions lib/exquickbooks/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ defmodule ExQuickBooks.Endpoint do
quote do
import unquote(__MODULE__), only: [
sign_request: 1,
sign_request: 3,
sign_request: 2,
send_request: 1
]

Expand All @@ -35,28 +35,14 @@ defmodule ExQuickBooks.Endpoint do
}
end

def sign_request(request) do
def sign_request(request = %Request{}, token \\ %{}) do
credentials =
ExQuickBooks.credentials
token
|> Map.delete(:__struct__)
|> Map.merge(ExQuickBooks.credentials)
|> Map.to_list
|> OAuther.credentials

sign_request(request, credentials)
end

def sign_request(request = %Request{}, token, token_secret) do
credentials =
ExQuickBooks.credentials
|> Keyword.merge([token: token, token_secret: token_secret])
|> OAuther.credentials

sign_request(request, credentials)
end

def send_request(request = %Request{}) do
ExQuickBooks.backend.request(request)
end

defp sign_request(request = %Request{}, credentials) do
{header, new_params} =
request.method
|> to_string
Expand All @@ -68,4 +54,8 @@ defmodule ExQuickBooks.Endpoint do

%{request | headers: new_headers, options: new_options}
end

def send_request(request = %Request{}) do
ExQuickBooks.backend.request(request)
end
end
104 changes: 43 additions & 61 deletions lib/exquickbooks/oauth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,21 @@ defmodule ExQuickBooks.OAuth do
## Request token
To start the authentication flow, your application needs to get a request
token and secret using `get_request_token/0`:
token using `get_request_token/0`:
```
{:ok,
%{"oauth_token" => request_token,
"oauth_token_secret" => request_token_secret},
redirect_url} = ExQuickBooks.get_request_token
{:ok, request_token, redirect_url} = ExQuickBooks.get_request_token
```
That function will also give you the URL where you should redirect the user
to authorise your application to access their QuickBooks data. After that
step they will be redirected to the `:callback_url` you’ve set in the
configuration.
The token is an `ExQuickBooks.Token`, see its documentation for more details.
If you need to persist data (such as the request token and secret) between
this request and the callback, you could store that data e.g. in the current
user’s (encrypted!) session.
You will also receive a URL where you should redirect the user to authorise
your application to access their QuickBooks data. After that step they are
redirected to the `:callback_url` you’ve set in the configuration.
If you need to persist data (such as the request token) between this request
and the callback, you could store that data e.g. in the current user’s
(encrypted) session.
## Callback
Expand All @@ -35,87 +33,71 @@ defmodule ExQuickBooks.OAuth do
- `"realmId"` -
ID of the user’s QuickBooks realm. Note the camel-cased name.
- `"oauth_verifier"` -
Verification string you can use to retrieve access credentials.
Token verifier string you can use to retrieve an access token.
There are more parameters as well, but these are most relevant.
## Access token
You can pass the verifier with the previous request token to
`get_access_token/3` in order to retrieve an access token and secret:
You can now exchange the request token and the verifier from the callback
request parameters for an access token using `get_access_token/2`:
```
{:ok,
%{"oauth_token" => access_token,
"oauth_token_secret" => access_token_secret}} =
ExQuickBooks.get_access_token(request_token, request_token_secret, verifier)
{:ok, access_token} = ExQuickBooks.get_access_token(request_token, verifier)
```
Your application should now store the realm ID, access token, and secret. Use
them in API calls to authenticate on behalf of the user.
The token is an `ExQuickBooks.Token`, see its documentation for more details.
Now you should store the realm ID and access token. Use them in API calls to
authenticate on behalf of the user.
"""

use ExQuickBooks.Endpoint, base_url: ExQuickBooks.oauth_api

@type response_body :: %{required(String.t) => String.t}
alias ExQuickBooks.Token

@doc """
Retrieves a new OAuth request token.
Returns the token response and a URL where your application should redirect
the user.
Retrieves a new request token.
The response body contains the following keys:
- `"oauth_token"` -
The request token associated with the user.
- `"oauth_token_secret"` -
The request token secret associated with the user.
Note that the redirect URL is prepopulated with the request token.
Returns the request token and a URL where your application should redirect
the user. Note that the redirect URL is already prepopulated with the request
token.
"""
@spec get_request_token :: {:ok, response_body, String.t} | {:error, any}
@spec get_request_token :: {:ok, Token.t, String.t} | {:error, any}
def get_request_token do
result =
request(:post, "get_request_token", nil, nil, params: [
{"oauth_callback", ExQuickBooks.callback_url}
])
|> sign_request()
|> sign_request
|> send_request

with {:ok, response} <- result,
{:ok, body} <- parse_body(response),
{:ok, body} <- parse_token_response(body),
do: {:ok, body, redirect_url(body)}
{:ok, token} <- parse_token(body),
do: {:ok, token, redirect_url(token)}
end

@doc """
Exchanges an authorised request token and a token verifier for an access
token. The secret is used for signing the request.
The token verifier required with this call was returned previously with the
callback URL params.
The response body contains the following keys:
Exchanges a request token and a token verifier for an access token.
- `"oauth_token"` -
The access token associated with the user.
- `"oauth_token_secret"` -
The access token secret associated with the user.
You should have previously received the token verifier in the callback URL
params as `"oauth_verifier"`.
"""
@spec get_access_token(String.t, String.t, String.t) ::
{:ok, response_body} | {:error, any}
def get_access_token(request_token, request_token_secret, verifier) do
@spec get_access_token(Token.t, String.t) :: {:ok, Token.t} | {:error, any}
def get_access_token(request_token = %Token{}, verifier) do
result =
request(:post, "get_access_token", nil, nil, params: [
{"oauth_token", request_token},
{"oauth_token", request_token.token},
{"oauth_verifier", verifier}
])
|> sign_request(request_token, request_token_secret)
|> sign_request(request_token)
|> send_request

with {:ok, response} <- result,
{:ok, body} <- parse_body(response),
{:ok, body} <- parse_token_response(body),
do: {:ok, body}
{:ok, token} <- parse_token(body),
do: {:ok, token}
end

defp parse_body(%{body: body}) when is_binary(body) do
Expand All @@ -125,17 +107,17 @@ defmodule ExQuickBooks.OAuth do
{:error, "Response body was malformed."}
end

defp parse_token_response(body = %{"oauth_token" => _}) do
{:ok, body}
defp parse_token(%{"oauth_token" => token, "oauth_token_secret" => secret}) do
{:ok, %Token{token: token, token_secret: secret}}
end
defp parse_token_response(body = %{"oauth_problem" => _}) do
defp parse_token(body = %{"oauth_problem" => _}) do
{:error, body}
end
defp parse_token_response(_) do
defp parse_token(_) do
{:error, "Response body did not contain oauth_token or oauth_problem."}
end

defp redirect_url(%{"oauth_token" => token}) do
defp redirect_url(%Token{token: token}) do
"https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token}"
end
end
37 changes: 37 additions & 0 deletions lib/exquickbooks/token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
defmodule ExQuickBooks.Token do
@moduledoc """
OAuth 1.0a token/secret pair for authenticating API calls.
See `ExQuickBooks.OAuth` for documentation on how to obtain these tokens.
## Structure
The token is technically a public/private key pair:
- `:token` - The public key binary.
- `:token_secret` - The private key binary.
The token is sent with API requests, but the secret is only used to calculate
the request signatures.
## Storing the token
You can store the token as is if your storage supports maps (in a Postgres
`jsonb` column, for example) or store the keys individually.
If you’re storing the token in a user’s session, make sure the session
storage is encrypted.
"""

@type t :: %__MODULE__{
token: binary,
token_secret: binary
}

@enforce_keys [
:token,
:token_secret
]

defstruct @enforce_keys
end
5 changes: 4 additions & 1 deletion test/exquickbooks/endpoint_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ defmodule ExQuickBooks.EndpointTest do
use ExQuickBooks.Endpoint, base_url: "http://localhost/"

alias ExQuickBooks.Request
alias ExQuickBooks.Token

doctest ExQuickBooks.Endpoint

Expand All @@ -21,9 +22,11 @@ defmodule ExQuickBooks.EndpointTest do
end

test "sign_request/3 signs with consumer credentials and the token" do
token = %Token{token: "token", token_secret: "secret"}

assert %Request{
headers: [{"Authorization", "OAuth " <> authorization}]
} = request(:get, "foo") |> sign_request("token", "secret")
} = request(:get, "foo") |> sign_request(token)

assert String.contains?(authorization, "oauth_consumer_key")
assert String.contains?(authorization, "oauth_token")
Expand Down
35 changes: 18 additions & 17 deletions test/exquickbooks/oauth_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ defmodule ExQuickBooks.OAuthTest do
use ExQuickBooks.APICase

alias ExQuickBooks.OAuth
alias ExQuickBooks.Token

doctest OAuth

Expand All @@ -11,44 +12,44 @@ defmodule ExQuickBooks.OAuthTest do

assert OAuth.get_request_token ==
{:ok,
%{"oauth_token" => "token",
"oauth_token_secret" => "secret",
"oauth_callback_confirmed" => "true"},
"https://appcenter.intuit.com/Connect/Begin?oauth_token=token"}
%Token{token: "token", token_secret: "secret"},
"https://appcenter.intuit.com/Connect/Begin?oauth_token=token"}
end

test "get_request_token/0 recovers when there's an OAuth problem" do
load_response("oauth/get_request_token_problem") |> send_response

assert OAuth.get_request_token ==
{:error,
%{"oauth_problem" => "signature_invalid"}}
{:error, %{"oauth_problem" => "signature_invalid"}}
end

test "get_request_token/0 recovers when there's some other error" do
http_400_response() |> send_response
assert {:error, _} = OAuth.get_request_token
end

test "get_access_token/3 returns token" do
test "get_access_token/2 returns token" do
token = %Token{token: "token", token_secret: "secret"}

load_response("oauth/get_access_token") |> send_response

assert OAuth.get_access_token("token", "secret", "verifier") ==
{:ok,
%{"oauth_token" => "token",
"oauth_token_secret" => "secret"}}
assert OAuth.get_access_token(token, "verifier") == {:ok, token}
end

test "get_access_token/3 recovers when there's an OAuth problem" do
test "get_access_token/2 recovers when there's an OAuth problem" do
token = %Token{token: "token", token_secret: "secret"}

load_response("oauth/get_access_token_problem") |> send_response

assert OAuth.get_access_token("token", "secret", "verifier") ==
{:error,
%{"oauth_problem" => "signature_invalid"}}
assert OAuth.get_access_token(token, "verifier") ==
{:error, %{"oauth_problem" => "signature_invalid"}}
end

test "get_access_token/3 recovers when there's some other error" do
test "get_access_token/2 recovers when there's some other error" do
token = %Token{token: "token", token_secret: "secret"}

http_400_response() |> send_response
assert {:error, _} = OAuth.get_access_token("token", "secret", "verifier")

assert {:error, _} = OAuth.get_access_token(token, "verifier")
end
end
8 changes: 4 additions & 4 deletions test/exquickbooks_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,10 @@ defmodule ExQuickBooksTest do
put_env :consumer_key, "key"
put_env :consumer_secret, "secret"

assert ExQuickBooks.credentials == [
assert ExQuickBooks.credentials == %{
consumer_key: "key",
consumer_secret: "secret"
]
}
end

test "credentials/0 raises for missing credentials" do
Expand Down Expand Up @@ -71,10 +71,10 @@ defmodule ExQuickBooksTest do
put_env :consumer_secret, {:system, "EXQUICKBOOKS_SECRET"}
put_env :use_production_api, {:system, "EXQUICKBOOKS_USE_PRODUCTION_API"}

assert ExQuickBooks.credentials == [
assert ExQuickBooks.credentials == %{
consumer_key: "system_key",
consumer_secret: "system_secret"
]
}

# Production config flag should be parsed as a boolean
refute ExQuickBooks.accounting_api |> String.contains?("sandbox")
Expand Down

0 comments on commit 7ff3329

Please sign in to comment.