Packages

A Slack Web API client generatef from their OpenAPI specs.

Current section

Files

Jump to
box lib box_oauth2.ex
Raw

lib/box_oauth2.ex

defmodule BoxOAuth2 do
use Tesla
@box_auth_url "https://api.box.com/oauth2/token"
@moduledoc """
Box OAuth2 authentication layer.
"""
plug(Tesla.Middleware.JSON, engine_opts: [keys: :atoms])
defstruct [
:authentication_url,
:client_id,
:enterprise_id,
:client_secret,
:private_key,
:public_key_id,
:token,
:expires_after,
:client
]
@type t :: %BoxOAuth2{
authentication_url: String.t(),
client_id: String.t(),
enterprise_id: String.t(),
client_secret: String.t(),
private_key: String.t(),
public_key_id: String.t(),
token: String.t(),
expires_after: DateTime.t(),
client: Tesla.Client.t()
}
@doc """
new BoxAuth2 configured from the application environment.
"""
@spec new(String.t()) :: BoxOAuth2.t()
def new(base_url \\ @box_auth_url) do
%BoxOAuth2{
authentication_url: base_url,
client_id: client_id(),
enterprise_id: enterprise_id(),
client_secret: client_secret(),
private_key: private_key(private_key_path(), private_key_password()),
public_key_id: public_key_id()
}
end
@doc """
login does an initial login or refreshes the OAuth token if it expired.
"""
@spec login(BoxOAuth2.t()) :: BoxOAuth2.t()
def login(auth) do
if is_nil(auth.token) do
perform_login(auth)
else
case DateTime.compare(DateTime.utc_now(), auth.expires_after) do
:lt ->
{:ok, auth}
_ ->
perform_login(auth)
end
end
end
@token_exchange "urn:ietf:params:oauth:grant-type:token-exchange"
@access_token_type "urn:ietf:params:oauth:token-type:access_token"
@doc """
downscope_token creates a token with limited access to a particular file or folder.
"""
@spec downscope_token(BoxOAuth2.t(), list(), String.t()) :: String.t()
def downscope_token(auth, scopes, resource) do
form =
URI.encode_query(%{
scope: Enum.join(scopes, " "),
resource: resource,
grant_type: @token_exchange,
subject_token: auth.token,
subject_token_type: @access_token_type
})
{:ok, response} = Tesla.post(auth.client, auth.authentication_url, form)
if response.status == 200 do
token = response.body.access_token
dt =
DateTime.utc_now()
|> DateTime.add(response.body.expires_in, :second)
{:ok, %{token: token, expires_after: dt}}
else
{:error, response.status, response.body}
end
end
@spec perform_login(BoxOAuth2.t()) :: BoxAuth2.t()
defp perform_login(auth) do
{:ok, assertion} = bearer_token(auth)
form =
URI.encode_query(%{
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
# Our JWT assertion
assertion: assertion,
# The OAuth 2 client ID and secret
client_id: auth.client_id,
client_secret: auth.client_secret
})
{:ok, response} = post(auth.authentication_url, form)
if response.status == 200 do
token = response.body.access_token
dt =
DateTime.utc_now()
|> DateTime.add(response.body.expires_in, :second)
middleware = [
{Tesla.Middleware.JSON, [engine_opts: [keys: :atoms]]},
{Tesla.Middleware.Headers, [{"Authorization", "Bearer " <> token}]}
]
client = Tesla.client(middleware)
{:ok, %BoxOAuth2{auth | token: token, expires_after: dt, client: client}}
else
{:error, response.status, response.body}
end
end
defp client_id, do: Application.get_env(:box, :client_id)
defp enterprise_id, do: Application.get_env(:box, :enterprise_id)
defp public_key_id, do: Application.get_env(:box, :public_key_id)
defp client_secret, do: Application.get_env(:box, :client_secret)
defp private_key_path, do: Application.get_env(:box, :private_key_path)
defp private_key_password, do: Application.get_env(:box, :private_key_password)
defp bearer_token(auth) do
signer =
Joken.Signer.create("RS512", %{"pem" => auth.private_key}, %{kid: auth.public_key_id})
Joken.Signer.sign(claims(auth), signer)
end
defp claims(auth) do
# jti is an identifier that helps protect against replay attacks:
jti = :crypto.strong_rand_bytes(64) |> Base.encode16(case: :lower)
# give the assertion a lifetime of 45 seconds before it expires:
exp = DateTime.to_unix(DateTime.utc_now(), :second) + 45
%{
iss: auth.client_id,
sub: auth.enterprise_id,
box_sub_type: "enterprise",
aud: auth.authentication_url,
jti: jti,
exp: exp
}
end
defp private_key(path, passphrase) do
key_string = File.read!(path)
{:ok, key} = ExPublicKey.loads(key_string, passphrase)
{:ok, private_key} = ExPublicKey.pem_encode(key)
private_key
end
end