Skip to content

Commit

Permalink
Merge pull request #10 from Boulevard/lnikkila/token-structs
Browse files Browse the repository at this point in the history
Use separate structs for request and access tokens
  • Loading branch information
lnikkila authored Mar 14, 2017
2 parents a1c7442 + 42ae5f8 commit e6245ff
Show file tree
Hide file tree
Showing 10 changed files with 166 additions and 103 deletions.
19 changes: 11 additions & 8 deletions lib/exquickbooks/token.ex → lib/exquickbooks/access_token.ex
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
defmodule ExQuickBooks.Token do
defmodule ExQuickBooks.AccessToken do
@moduledoc """
OAuth 1.0a token/secret pair for authenticating API calls.
Expand All @@ -8,8 +8,12 @@ defmodule ExQuickBooks.Token do
The token is technically a public/private key pair:
- `:token` - The public key binary.
- `:token_secret` - The private key binary.
- `:token` -
The public key binary.
- `:token_secret` -
The private key binary.
- `:realm_id` -
ID of the Realm this token is associated with.
The token is sent with API requests, but the secret is only used to calculate
the request signatures.
Expand All @@ -18,19 +22,18 @@ defmodule ExQuickBooks.Token do
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
token_secret: binary,
realm_id: binary
}

@enforce_keys [
:token,
:token_secret
:token_secret,
:realm_id
]

defstruct @enforce_keys
Expand Down
2 changes: 1 addition & 1 deletion lib/exquickbooks/endpoint.ex
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ defmodule ExQuickBooks.Endpoint do
def sign_request(request = %Request{}, token \\ %{}) do
credentials =
token
|> Map.delete(:__struct__)
|> Map.take([:token, :token_secret])
|> Map.merge(ExQuickBooks.credentials)
|> Map.to_list
|> OAuther.credentials
Expand Down
20 changes: 10 additions & 10 deletions lib/exquickbooks/item.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,29 +9,29 @@ defmodule ExQuickBooks.Item do
use ExQuickBooks.Endpoint, base_url: ExQuickBooks.accounting_api
use ExQuickBooks.JSONEndpoint

alias ExQuickBooks.Token
alias ExQuickBooks.AccessToken

@doc """
Creates an item.
The item name must be unique. Sales items must define `IncomeAccountRef`.
Purchase items must define `ExpenseAccountRef`.
"""
@spec create_item(Token.t, String.t, json_map) ::
@spec create_item(AccessToken.t, json_map) ::
{:ok, json_map} | {:error, any}
def create_item(token, realm_id, item) do
request(:post, "company/#{realm_id}/item", item)
def create_item(token, item) do
request(:post, "company/#{token.realm_id}/item", item)
|> sign_request(token)
|> send_json_request
end

@doc """
Retrieves an item.
"""
@spec read_item(Token.t, String.t, String.t) ::
@spec read_item(AccessToken.t, String.t) ::
{:ok, json_map} | {:error, any}
def read_item(token, realm_id, item_id) do
request(:get, "company/#{realm_id}/item/#{item_id}")
def read_item(token, item_id) do
request(:get, "company/#{token.realm_id}/item/#{item_id}")
|> sign_request(token)
|> send_json_request
end
Expand All @@ -43,10 +43,10 @@ defmodule ExQuickBooks.Item do
`read_item/3`, otherwise the omitted values are set to their default values
or NULL.
"""
@spec update_item(Token.t, String.t, json_map) ::
@spec update_item(AccessToken.t, json_map) ::
{:ok, json_map} | {:error, any}
def update_item(token, realm_id, item) do
request(:post, "company/#{realm_id}/item", item)
def update_item(token, item) do
request(:post, "company/#{token.realm_id}/item", item)
|> sign_request(token)
|> send_json_request
end
Expand Down
68 changes: 44 additions & 24 deletions lib/exquickbooks/oauth.ex
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ defmodule ExQuickBooks.OAuth do
token using `get_request_token/0`:
```
{:ok, request_token, redirect_url} = ExQuickBooks.get_request_token
{:ok, request_token} = ExQuickBooks.get_request_token
```
The token is an `ExQuickBooks.Token`, see its documentation for more details.
The token is an `ExQuickBooks.RequestToken`, see its documentation for more
details.
You will also receive a URL where you should redirect the user to authorise
You should redirect the user to `request_token.redirect_url` 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.
Expand All @@ -39,31 +40,31 @@ defmodule ExQuickBooks.OAuth do
## Access token
You can now exchange the request token and the verifier from the callback
request parameters for an access token using `get_access_token/2`:
You can now exchange the request token, realm ID, and the verifier from the
callback request parameters for an access token using `get_access_token/3`:
```
{:ok, access_token} = ExQuickBooks.get_access_token(request_token, verifier)
{:ok, access_token} = ExQuickBooks.get_access_token(request_token, realm_id, verifier)
```
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.
Now you can store the access token and use it in API calls to authenticate on
behalf of the user. The token is an `ExQuickBooks.AccessToken`, see its
documentation for more details.
"""

use ExQuickBooks.Endpoint, base_url: ExQuickBooks.oauth_api

alias ExQuickBooks.Token
alias ExQuickBooks.AccessToken
alias ExQuickBooks.RequestToken

@doc """
Retrieves a new 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.
Returns the request token with a URL where your application should redirect
the user as `request_token.redirect_url`.
"""
@spec get_request_token :: {:ok, Token.t, String.t} | {:error, any}
@spec get_request_token ::
{:ok, RequestToken.t} | {:error, any}
def get_request_token do
result =
request(:post, "get_request_token", nil, nil, params: [
Expand All @@ -75,17 +76,18 @@ defmodule ExQuickBooks.OAuth do
with {:ok, response} <- result,
{:ok, body} <- parse_body(response),
{:ok, token} <- parse_token(body),
do: {:ok, token, redirect_url(token)}
do: {:ok, create_request_token(token)}
end

@doc """
Exchanges a request token and a token verifier for an access token.
Exchanges a request token, realm ID, and token verifier for an access token.
You should have previously received the token verifier in the callback URL
params as `"oauth_verifier"`.
You should have previously received the realm ID and token verifier in the
callback URL params as `"realmId"` and `"oauth_verifier"`.
"""
@spec get_access_token(Token.t, String.t) :: {:ok, Token.t} | {:error, any}
def get_access_token(request_token = %Token{}, verifier) do
@spec get_access_token(RequestToken.t, String.t, String.t) ::
{:ok, AccessToken.t} | {:error, any}
def get_access_token(request_token = %RequestToken{}, realm_id, verifier) do
result =
request(:post, "get_access_token", nil, nil, params: [
{"oauth_token", request_token.token},
Expand All @@ -97,7 +99,7 @@ defmodule ExQuickBooks.OAuth do
with {:ok, response} <- result,
{:ok, body} <- parse_body(response),
{:ok, token} <- parse_token(body),
do: {:ok, token}
do: {:ok, create_access_token(token, realm_id)}
end

defp parse_body(%{body: body}) when is_binary(body) do
Expand All @@ -108,7 +110,7 @@ defmodule ExQuickBooks.OAuth do
end

defp parse_token(%{"oauth_token" => token, "oauth_token_secret" => secret}) do
{:ok, %Token{token: token, token_secret: secret}}
{:ok, %{token: token, token_secret: secret}}
end
defp parse_token(body = %{"oauth_problem" => _}) do
{:error, body}
Expand All @@ -117,7 +119,25 @@ defmodule ExQuickBooks.OAuth do
{:error, "Response body did not contain oauth_token or oauth_problem."}
end

defp redirect_url(%Token{token: token}) do
defp create_request_token(token) do
values =
token
|> Map.put(:redirect_url, redirect_url(token))
|> Map.to_list

struct!(RequestToken, values)
end

defp create_access_token(token, realm_id) do
values =
token
|> Map.put(:realm_id, realm_id)
|> Map.to_list

struct!(AccessToken, values)
end

defp redirect_url(%{token: token}) do
"https://appcenter.intuit.com/Connect/Begin?oauth_token=#{token}"
end
end
18 changes: 9 additions & 9 deletions lib/exquickbooks/preferences.ex
Original file line number Diff line number Diff line change
Expand Up @@ -9,30 +9,30 @@ defmodule ExQuickBooks.Preferences do
use ExQuickBooks.Endpoint, base_url: ExQuickBooks.accounting_api
use ExQuickBooks.JSONEndpoint

alias ExQuickBooks.Token
alias ExQuickBooks.AccessToken

@doc """
Retrieves preferences for the given realm.
Retrieves preferences for the realm.
"""
@spec read_preferences(Token.t, String.t) ::
@spec read_preferences(AccessToken.t) ::
{:ok, json_map} | {:error, any}
def read_preferences(token, realm_id) do
request(:get, "company/#{realm_id}/preferences")
def read_preferences(token) do
request(:get, "company/#{token.realm_id}/preferences")
|> sign_request(token)
|> send_json_request
end

@doc """
Updates and retrieves preferences for the given realm.
Updates and retrieves preferences for the realm.
This operation performs a full update. The preferences map must define all of
the keys in the full preferences map returned by `read_preferences/2`,
otherwise the omitted values are set to their default values or NULL.
"""
@spec update_preferences(Token.t, String.t, json_map) ::
@spec update_preferences(AccessToken.t, json_map) ::
{:ok, json_map} | {:error, any}
def update_preferences(token, realm_id, preferences) do
request(:post, "company/#{realm_id}/preferences", preferences)
def update_preferences(token, preferences) do
request(:post, "company/#{token.realm_id}/preferences", preferences)
|> sign_request(token)
|> send_json_request
end
Expand Down
40 changes: 40 additions & 0 deletions lib/exquickbooks/request_token.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule ExQuickBooks.RequestToken do
@moduledoc """
OAuth 1.0a token/secret pair for requesting an access token.
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.
- `:redirect_url` -
URL where you should redirect the user to continue authentication.
The token is sent with API requests, but the secret is only used to calculate
the request signatures.
## Storing the token
You can store this token in the user’s session until the OAuth callback. Make
sure the session storage is encrypted.
"""

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

@enforce_keys [
:token,
:token_secret,
:redirect_url
]

defstruct @enforce_keys
end
10 changes: 7 additions & 3 deletions test/exquickbooks/endpoint_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ defmodule ExQuickBooks.EndpointTest do
use ExQuickBooks.APICase
use ExQuickBooks.Endpoint, base_url: "http://localhost/"

alias ExQuickBooks.AccessToken
alias ExQuickBooks.Request
alias ExQuickBooks.Token

doctest ExQuickBooks.Endpoint

Expand All @@ -21,8 +21,12 @@ defmodule ExQuickBooks.EndpointTest do
refute String.contains?(authorization, "oauth_token")
end

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

assert %Request{
headers: [{"Authorization", "OAuth " <> authorization}]
Expand Down
Loading

0 comments on commit e6245ff

Please sign in to comment.