forked from peburrows/goth
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
fbd7901
commit 4807dfc
Showing
19 changed files
with
777 additions
and
445 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
Oops, something went wrong.