defmodule Vault.Auth.Generic do
@moduledoc """
A Generic Auth Adapter. An alternative to writing your own adapter.
"""
@type request :: %{
path: String.t(),
method: :post,
body: map()
}
@type response :: %{
token: list(String.t()),
ttl: list(String.t())
}
@type params :: %{
request: request(),
response: response()
}
@behaviour Vault.Auth.Adapter
@doc """
Authenticate with a custom auth method. Provide options for the request, and how
to parse the response.
## Examples
`request` defines parameters for the request to vault
- `path`- the path for authentication, after "auth" If you want to authenticate against `https://myvault.com/v1/auth/jwt/login`, then the path would be `jwt/login`
- `method`- one of `:get`, `:post`, `:put`, `:patch`, `:delete`, defaults to `:post`
- `body`- any params needed to login. Defaults to `%{}`
`response` defines parameters for parsing the response.
- `token_path` - a list of properties that describe the JSON path to a token. Defaults to `["auth", "client_token"]`
- `ttl_path` - a list of properties that describe the JSON path to the ttl, or lease duration. Defaults to ["auth", "lease_duration"]
The following would provide a minimal adapter for the JWT backend:
```
{:ok, token, ttl} = Vault.Auth.Generic.login(vault, %{
request: %{
path: "/jwt/login",
body: %{role: "my-role", jwt: "my-jwt" },
}
})
```
Here's the above example as part of the full Vault client flow. On success,
it returns an authenticated vault client.
```
vault =
Vault.new([
auth: Vault.Auth.Generic,
http: Vault.HTTP.Tesla,
engine: Vault.KVV2
])
{:ok, vault} = Vault.auth(vault, %{
request: %{
path: "/jwt/login",
body: %{role: "my-role", jwt: "my-jwt" },
}
})
```
Here's a more explicit example, with every option configured.
```
vault =
Vault.new([
auth: Vault.Auth.Generic,
http: Vault.HTTP.Tesla,
engine: Vault.KVV2
])
{:ok, vault} = Vault.auth(vault, %{
request:
path: "/jwt/login",
method: :post,
body: %{role: "my-role", jwt: "my-jwt" },
response: %{
token: ["auth", "client_token"],
ttl: ["auth", "lease_duration"]
}
})
```
"""
@default_response %{token: ["auth", "client_token"], ttl: ["auth", "lease_duration"]}
@impl true
@spec login(Vault.t(), params) :: Vault.Auth.Adapter.response()
def login(vault, params)
def login(%Vault{} = vault, %{request: request} = params) do
request = Map.merge(%{method: :post, body: %{}}, request)
response = Map.merge(@default_response, Map.get(params, :response, %{}))
headers = [
{"Content-Type", "application/json"}
]
url = "auth/#{request.path}"
with {:ok, http_response} <-
Vault.HTTP.request(vault, request.method, url, body: request.body, headers: headers) do
case http_response do
%{"errors" => []} ->
{:error, ["Key not found"]}
%{"errors" => messages} ->
{:error, messages}
otherwise ->
token = get_in(otherwise, response.token)
ttl = get_in(otherwise, response.ttl)
if token && ttl do
{:ok, token, ttl}
else
{:error, ["Unexpected response from vault.", otherwise]}
end
end
else
{:error, reason} ->
{:error, ["Http adapter error", inspect(reason)]}
end
end
end