Skip to content

Commit

Permalink
Add new API (peburrows#83)
Browse files Browse the repository at this point in the history
  • Loading branch information
wojtekmach authored Jan 27, 2021
1 parent fbd7901 commit 4807dfc
Show file tree
Hide file tree
Showing 19 changed files with 777 additions and 445 deletions.
185 changes: 96 additions & 89 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,114 +2,121 @@

# Goth

<!-- MDOC !-->

Google + Auth = Goth

A simple library to generate and retrieve OAuth2 tokens for use with Google Cloud Service accounts.

It can either retrieve tokens using service account credentials or from Google's metadata service for applications running on Google Cloud Platform.

## Installation

1. Add Goth to your list of dependencies in `mix.exs`:
```elixir
def deps do
[{:goth, "~> 1.2.0"}]
end
```

2. Pass in your credentials json downloaded from your GCE account:

```elixir
config :goth,
json: "path/to/google/json/creds.json" |> File.read!
```

Or, via an ENV var:
```elixir
config :goth, json: {:system, "GCP_CREDENTIALS"}
```

Or, via your own config module:
```elixir
config :goth, config_module: MyConfigMod
```
```elixir
defmodule MyConfigMod do
use Goth.Config

def init(config) do
{:ok, Keyword.put(config, :json, System.get_env("MY_GCP_JSON_CREDENTIALS"))}
end
end
```

You can also use a JSON file containing an array of service accounts to be able to use different identities in your application. Each service
account will be identified by its ```client_email```, which can be passed to ```Goth.Token.for_scope/1``` to specify which service account to use.

For example, if your JSON file contains the following:

```json
[
{
"client_email": "[email protected]",
...
},
{
"client_email": "[email protected]",
...
}
]
```

You can use the following to get a token for the second service account:
```elixir
def deps do
[{:goth, "~> 1.3"}]
end
```

2. Add Goth to your supervision tree:

```elixir
defmodule MyApp.Application do
use Application

def start(_type, _args) do
credentials = "GOOGLE_APPLICATION_CREDENTIALS_JSON" |> System.fetch_env!() |> Jason.decode!()

children = [
{Goth, name: MyApp.Goth, credentials: credentials}
]

Supervisor.start_link(children, strategy: :one_for_one)
end
end
```

3. Fetch the token:

```elixir
iex> {:ok, token} = Goth.fetch(MyApp.Goth)
iex> token
%Goth.Token{
expires: 1453356568,
token: "ya29.cALlJ4ICWRvMkYB-WsAR-CZnExE459PA7QPqKg5nei9y2T9-iqmbcgxq8XrTATNn_BPim",
type: "Bearer"
}
```

<!-- MDOC !-->

## Upgrading from Goth < 1.3

Earlier versions of Goth relied on global application environment configuration which is deprecated
in favour of a more direct and explicit approach in Goth v1.3+.

You might have code similar to this:

```elixir
# config/config.exs
config :goth,
json: {:system, "GCP_CREDENTIALS"}
```

```elixir
def get_token do
{:ok, token} = Goth.Token.for_scope({
"[email protected]",
"https://www.googleapis.com/auth/cloud-platform.read-only"})
# lib/myapp.ex
defmodule MyApp do
def gcloud_authorization() do
{:ok, token} = Goth.Token.for_scope("https://www.googleapis.com/auth/cloud-platform.read-only")
"#{token.type} #{token.token}"
end
end
```

You can skip the last step if your application will run on a GCP or GKE instance with appropriate permissions.
Replace it with:

If you need to set the email account to impersonate. For example when using service accounts
```elixir
defmodule MyApp.Application do
@moduledoc false
use Application

```elixir
config :goth,
json: {:system, "GCP_CREDENTIALS"},
actor_email: "[email protected]"
```
def start(_type, _args) do
credentials = "GCP_CREDENTIALS" |> System.fetch_env!() |> Jason.decode!()

Alternatively, you can pass your sub email on a per-call basis, for example:
children = [
{Goth, name: MyApp.Goth, credentials: credentials}
]

```elixir
Goth.Token.for_scope("https://www.googleapis.com/auth/pubsub",
"[email protected]")
```
Supervisor.start_link(children, strategy: :one_for_one)
end
end
```

If you need to disable Goth in certain environments, you can set a `disabled`
flag in your config:
```elixir
# lib/myapp.ex
defmodule MyApp do
def gcloud_authorization() do
{:ok, token} = Goth.fetch(MyApp.Goth)
"#{token.type} #{token.token}"
end
end
```

```elixir
config :goth,
disabled: true
```
For more information on earlier versions of Goth, [see v1.2.0 documentation on hexdocs.pm](https://hexdocs.pm/goth/1.2.0).

This initializes Goth with an empty config, so any attempts to actually generate
tokens will fail.
# TODO

## Usage
We can close these tickets:

### Retrieve a token:
Call `Token.for_scope/1` passing in a string of [scopes](https://developers.google.com/identity/protocols/googlescopes), separated by a space:
```elixir
alias Goth.Token
{:ok, token} = Token.for_scope("https://www.googleapis.com/auth/pubsub")
#=>
%Goth.Token{
expires: 1453356568,
token: "ya29.cALlJ4ICWRvMkYB-WsAR-CZnExE459PA7QPqKg5nei9y2T9-iqmbcgxq8XrTATNn_BPim",
type: "Bearer"
}
```
* https://github.com/peburrows/goth/issues/23, https://github.com/peburrows/goth/pull/54 - `:http_opts` option on Goth.start_link/1 and Goth.Token.fetch/1
* https://github.com/peburrows/goth/issues/35
* https://github.com/peburrows/goth/issues/53 - seems a problem with Goth.Config, can be closed as we have new api
* https://github.com/peburrows/goth/issues/57 - we now have a slightly better error message, that the expected shape doesn't match
* https://github.com/peburrows/goth/issues/65 - they can start different Goth instances for different test scenarios. Or use Goth.Token.fetch/1 directly to bypass the cache.
* https://github.com/peburrows/goth/issues/67
* https://github.com/peburrows/goth/issues/69
* https://github.com/peburrows/goth/issues/72 - bug with older Hackney on newer OTP
* https://github.com/peburrows/goth/issues/77, https://github.com/peburrows/goth/pull/79 - do we want to support this, or users would explicitly load from GOOGLE_APPLICATION_CREDENTIALS env or ~/.config/gcloud/application_default_credentials.json in their supervision tree?
* https://github.com/peburrows/goth/pull/66 - `:refresh_before` option on `Goth.start_link/1`.
* https://github.com/peburrows/goth/pull/76
* https://github.com/peburrows/goth/pull/80
32 changes: 2 additions & 30 deletions config/config.exs
Original file line number Diff line number Diff line change
@@ -1,30 +1,2 @@
# This file is responsible for configuring your application
# and its dependencies with the aid of the Mix.Config module.
use Mix.Config

# This configuration is loaded before any dependency and is restricted
# to this project. If another project depends on this project, this
# file won't be loaded nor affect the parent project. For this reason,
# if you want to provide default values for your application for
# 3rd-party users, it should be done in your "mix.exs" file.

# You can configure for your application as:
#
# config :goth, key: :value
#
# And access this configuration in your application as:
#
# Application.get_env(:goth, :key)
#
# Or configure a 3rd-party app:
#
# config :logger, level: :info
#

# It is also possible to import configuration files, relative to this
# directory. For example, you can emulate configuration per environment
# by uncommenting the line below and defining dev.exs, test.exs and such.
# Configuration from the imported file will override the ones defined
# here (which is why it is important to import them last).
#
import_config "#{Mix.env}.exs"
import Config
import_config "#{Mix.env()}.exs"
9 changes: 1 addition & 8 deletions config/dev.exs
Original file line number Diff line number Diff line change
@@ -1,8 +1 @@
use Mix.Config

try do
config :goth,
json: "config/dev-credentials.json" |> Path.expand |> File.read!
rescue
_ -> :ok
end
import Config
3 changes: 2 additions & 1 deletion config/test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use Mix.Config

config :goth,
json: "test/data/test-credentials.json" |> Path.expand |> File.read!
json: "test/data/test-credentials.json" |> Path.expand() |> File.read!()

config :goth, config_root_dir: "test/missing"

# config :bypass, enable_debug_log: true
76 changes: 69 additions & 7 deletions lib/goth.ex
Original file line number Diff line number Diff line change
@@ -1,13 +1,75 @@
defmodule Goth do
use Application
@external_resource "README.md"

@moduledoc """
Google + Auth = Goth.
@moduledoc "README.md"
|> File.read!()
|> String.split("<!-- MDOC !-->")
|> Enum.fetch!(1)

@doc """
Fetches the token.
If the token is not in the cache, we immediately request it.
"""
@doc since: "1.3.0"
defdelegate fetch(server), to: Goth.Server

@scope "https://www.googleapis.com/auth/cloud-platform"
@url "https://www.googleapis.com/oauth2/v4/token"
@cooldown 1000
@refresh_before_minutes 5

@doc """
Starts the server.
When the server is started, we attempt to fetch the token and store it in
internal cache. If we fail, we'll try up to 3 times with #{@cooldown}ms
cooldown between requests and if we couldn't retrieve it, we crash.
## Options
* `:name` - the name to register the server under.
* `:credentials` - a map of credentials.
* `:cooldown` - Time in milliseconds between retrying requests, defaults
to `#{@cooldown}`.
* `:scope` - Token scope, defaults to `#{inspect(@scope)}`.
See https://developers.google.com/identity/protocols/oauth2/scopes for
available scopes.
* `:refresh_before` - Time in seconds before the token is about to expire
that it is tried to be automatically refreshed. Defaults to
`#{@refresh_before_minutes * 60}` (#{@refresh_before_minutes} minutes).
* `:url` - URL to fetch the token from, defaults to `#{inspect(@url)}`.
* `:http_opts` - Options passed to the underlying HTTP client, defaults to `[]`.
"""
@doc since: "1.3.0"
def start_link(opts) do
opts |> with_default_opts() |> Goth.Server.start_link()
end

@doc """
Returns a supervision child spec.
Accepts the same options as `start_link/1`.
"""
@doc since: "1.3.0"
def child_spec(opts) do
opts |> with_default_opts() |> Goth.Server.child_spec()
end

# for now, just spin up the supervisor
def start(_type, _args) do
envs = Application.get_all_env(:goth)
Goth.Supervisor.start_link(envs)
defp with_default_opts(opts) do
opts
|> Keyword.put_new(:scope, @scope)
|> Keyword.put_new(:url, @url)
|> Keyword.put_new(:cooldown, @cooldown)
|> Keyword.put_new(:refresh_before, @refresh_before_minutes * 60)
|> Keyword.put_new(:http_opts, [])
end
end
15 changes: 15 additions & 0 deletions lib/goth/application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
defmodule Goth.Application do
@moduledoc false
use Application

@impl true
def start(_type, _args) do
envs = Application.get_all_env(:goth)

if envs == [] do
Supervisor.start_link([], strategy: :one_for_one)
else
Goth.Supervisor.start_link(envs)
end
end
end
Loading

0 comments on commit 4807dfc

Please sign in to comment.