chore: copy code for release from private repo
This commit is contained in:
commit
81db5cdcad
4
.formatter.exs
Normal file
4
.formatter.exs
Normal file
@ -0,0 +1,4 @@
|
||||
# Used by "mix format"
|
||||
[
|
||||
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
|
||||
]
|
||||
26
.gitignore
vendored
Normal file
26
.gitignore
vendored
Normal file
@ -0,0 +1,26 @@
|
||||
# The directory Mix will write compiled artifacts to.
|
||||
/_build/
|
||||
|
||||
# If you run "mix test --cover", coverage assets end up here.
|
||||
/cover/
|
||||
|
||||
# The directory Mix downloads your dependencies sources to.
|
||||
/deps/
|
||||
|
||||
# Where third-party dependencies like ExDoc output generated docs.
|
||||
/doc/
|
||||
|
||||
# Ignore .fetch files in case you like to edit your project deps locally.
|
||||
/.fetch
|
||||
|
||||
# If the VM crashes, it generates a dump, let's ignore it too.
|
||||
erl_crash.dump
|
||||
|
||||
# Also ignore archive artifacts (built via "mix archive.build").
|
||||
*.ez
|
||||
|
||||
# Ignore package tarball (built via "mix hex.build").
|
||||
oauth2_token_manager-*.tar
|
||||
|
||||
# Temporary files, for example, from tests.
|
||||
/tmp/
|
||||
122
README.md
Normal file
122
README.md
Normal file
@ -0,0 +1,122 @@
|
||||
# OAuth2TokenManager
|
||||
|
||||
This package works with the `oauth2` package to manage the automatic renewal of
|
||||
tokens before they expire
|
||||
|
||||
## Installation
|
||||
|
||||
If [available in Hex](https://hex.pm/docs/publish), the package can be installed
|
||||
by adding `oauth2_token_manager` to your list of dependencies in `mix.exs`:
|
||||
|
||||
```elixir
|
||||
def deps do
|
||||
[
|
||||
{:oauth2_token_manager, "~> 0.1.0"}
|
||||
]
|
||||
end
|
||||
```
|
||||
|
||||
Documentation can be generated with [ExDoc](https://github.com/elixir-lang/ex_doc)
|
||||
and published on [HexDocs](https://hexdocs.pm). Once published, the docs can
|
||||
be found at <https://hexdocs.pm/oauth2_token_manager>.
|
||||
|
||||
## Usage
|
||||
|
||||
Create an `OAuth2.Client` instance to use to get the initial token and pass it to
|
||||
`OAuth2TokenMananger.TokenAgent.start_link/1` along with the inline
|
||||
`OAuth2TokenManager.TokenRefreshStrategy` and the name for the `Agent`.
|
||||
|
||||
The client can be configured as described at
|
||||
https://github.com/ueberauth/oauth2#configure-a-http-client.
|
||||
|
||||
```Elixir
|
||||
client = Client.new([
|
||||
strategy: OAuth2.Strategy.ClientCredentials,
|
||||
client_id: "example_client_id",
|
||||
client_secret: "example_client_secret",
|
||||
site: "https://example.com/"
|
||||
])
|
||||
|
||||
{:ok, agent} = OAuth2TokenManager.TokenAgent.start_link(
|
||||
name: MyModule.TokenAgent,
|
||||
initial_client: client,
|
||||
inline_refresh_strategy: %OAuth2TokenManager.TokenRefreshStrategy{seconds_before_expires: 30}
|
||||
)
|
||||
```
|
||||
|
||||
The current version of the client can be retrieved using
|
||||
`OAuth2TokenManager.TokenAgent.get_client/1` with the agent's PID or name.
|
||||
This client will automatically use the access token as a Bearer token in
|
||||
the Authorization header when calling its request methods. The access token
|
||||
itself can be retrieved from the client struct if it is desirable to use separately
|
||||
configured clients and `OAuth2TokenManager.TokenAgent.get_access_token/1` is
|
||||
provided for convenience when doing so.
|
||||
|
||||
```Elixir
|
||||
current_client = OAuth2TokenManager.TokenAgent.get_client(agent)
|
||||
current_client = OAuth2TokenManager.TokenAgent.get_client(MyModule.TokenAgent)
|
||||
access_token = OAuth2TokenManager.TokenAgent.get_access_token(MyModule.TokenAgent)
|
||||
```
|
||||
|
||||
The token can be refreshed by calling `OAuth2TokenManager.TokenAgent.refresh_tokens/1`.
|
||||
If a refresh token is available, it will be exchanged for a new set of tokens.
|
||||
If no refresh token is available or the attempt to use the refresh results in an error,
|
||||
the original client will be used again to attempt to obtain a new set of tokens.
|
||||
|
||||
```Elixir
|
||||
:ok = OAuth2TokenManager.TokenAgent.refresh(MyModule.TokenAgent)
|
||||
```
|
||||
|
||||
This can be used to handle complex token refresh logic, but it will generally be
|
||||
preferable to configure [a strategy](#inline-token-refresh-strategies).
|
||||
|
||||
### Inline Token Refresh Strategies
|
||||
|
||||
Inline token refresh strategies are used to determine when to obtain a new token
|
||||
before returning the current client state or token value. The supported conditions
|
||||
are show in the below example.
|
||||
|
||||
```Elixir
|
||||
%TokenRefreshStrategy{
|
||||
seconds_before_expires: 30, # refresh the token if it will expire in the next N seconds
|
||||
every_seconds: 300 # refresh the token if it has been at least N seconds since the last refresh
|
||||
}
|
||||
```
|
||||
|
||||
If the Authorization Server does not provide an expiration time for the token, the
|
||||
expiration time conditions will not trigger a refresh, so `:every_seconds` should
|
||||
be used.
|
||||
|
||||
The inline refreshes only occur as part of request for data from the agent; this
|
||||
saves unnecessary renewal requests in low-volume systems but tokens may be allowed
|
||||
to expire if unused. If refresh tokens need to be kept active in a system where the
|
||||
time between requests exceeds the token duration and the initial client cannot be
|
||||
reused, using `OAuth2TokenManager.TokenAgent.refresh` may be necessary.
|
||||
|
||||
The inline refreshes occur during message processing in the agent, which is not
|
||||
concurrent per agent. This guarantees that redundant refresh calls will not be
|
||||
made, which is particularly important when using single-use refresh tokens, but
|
||||
also adds the latency of the refresh to the processing of the message that
|
||||
triggers it.
|
||||
|
||||
The same strategy can have properties configured for multiple conditions and the
|
||||
token will be refreshed if any of them are met.
|
||||
|
||||
If no value is specified when starting the agent, the behavior defaults to
|
||||
`%TokenRefreshStrategy{seconds_before_expires: 30, every_seconds: 300}`.
|
||||
|
||||
### Configuring a Singleton
|
||||
|
||||
Providing a name for the agent allows it to be referred to in code without passing
|
||||
around the PID, which is useful for reusing the same token whenever providing
|
||||
credentials for the same principal, cutting down on the number of calls that need
|
||||
to be made to the Authorization Server. A named instance can be added to a
|
||||
supervision tree's children using a tuple like the below example. This will
|
||||
launch the agent under the supervision tree and restart it if it crashes.
|
||||
|
||||
```Elixir
|
||||
children = [
|
||||
{TokenAgent, name: MyModule.TokenAgent, initial_client: client}
|
||||
]
|
||||
|
||||
```
|
||||
158
lib/o_auth2_token_manager/token_agent.ex
Normal file
158
lib/o_auth2_token_manager/token_agent.ex
Normal file
@ -0,0 +1,158 @@
|
||||
defmodule OAuth2TokenManager.TokenAgent do
|
||||
@moduledoc """
|
||||
Defines the Agent used to manage the token and the struct it uses to store its state
|
||||
"""
|
||||
|
||||
use Agent
|
||||
use TypedStruct
|
||||
|
||||
alias __MODULE__
|
||||
alias OAuth2TokenManager.TokenRefreshStrategy
|
||||
alias OAuth2.{AccessToken, Client, Error, Response}
|
||||
|
||||
require Logger
|
||||
|
||||
@typedoc """
|
||||
Struct for tracking the state of the agent
|
||||
"""
|
||||
typedstruct do
|
||||
field(:name, String.t(), enforce: true)
|
||||
field(:initial_client, Client.t(), enforce: true)
|
||||
field(:client_with_token, Client.t(), enforce: true)
|
||||
field(:inline_refresh_strategy, TokenRefreshStrategy.t())
|
||||
field(:last_refreshed, Calendar.datetime(), enforce: true)
|
||||
end
|
||||
|
||||
@type option ::
|
||||
{:name, term()}
|
||||
| {:initial_client, Client.t()}
|
||||
| {:inline_refresh_strategy, TokenRefreshStrategy.t()}
|
||||
|
||||
@spec start_link([option()]) :: Agent.on_start() | {:error, Response.t()} | {:error, Error.t()}
|
||||
def start_link(opts) do
|
||||
case Keyword.fetch(opts, :initial_client) do
|
||||
:error ->
|
||||
{:error, ":initial_client required"}
|
||||
|
||||
{:ok, initial_client} ->
|
||||
inline_refresh_strategy =
|
||||
Keyword.get(opts, :inline_refresh_strategy, %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30,
|
||||
every_seconds: 300
|
||||
})
|
||||
|
||||
name = Keyword.get(opts, :name)
|
||||
|
||||
case Client.get_token(initial_client) do
|
||||
{:ok, client_with_token} ->
|
||||
Agent.start_link(
|
||||
fn ->
|
||||
%TokenAgent{
|
||||
name: name,
|
||||
initial_client: initial_client,
|
||||
client_with_token: client_with_token,
|
||||
inline_refresh_strategy: inline_refresh_strategy,
|
||||
last_refreshed: DateTime.utc_now()
|
||||
}
|
||||
end,
|
||||
name: name
|
||||
)
|
||||
|
||||
error ->
|
||||
error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current client instance; if :inline_updates is configured, the client will be refreshed first if the strategy indicates
|
||||
it needs to be
|
||||
"""
|
||||
@spec get_current_client(Agent.agent()) :: Client.t()
|
||||
def get_current_client(token_agent) do
|
||||
Agent.get_and_update(token_agent, fn state ->
|
||||
new_state =
|
||||
if state.inline_refresh_strategy &&
|
||||
TokenRefreshStrategy.refresh_now?(
|
||||
state.inline_refresh_strategy,
|
||||
state.last_refreshed,
|
||||
DateTime.from_unix!(state.client_with_token.token.expires_at)
|
||||
) do
|
||||
get_state_with_new_tokens(state)
|
||||
else
|
||||
state
|
||||
end
|
||||
|
||||
{new_state.client_with_token, new_state}
|
||||
end)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns the current access token; if :inline_updates is configured, the token will be refreshed first if the strategy indicates
|
||||
it needs to be
|
||||
"""
|
||||
@spec get_access_token(Agent.agent()) :: String.t()
|
||||
def get_access_token(token_agent) do
|
||||
get_current_client(token_agent).token.access_token
|
||||
end
|
||||
|
||||
@doc """
|
||||
Triggers a refresh of the agent's tokens
|
||||
"""
|
||||
@spec refresh(Agent.agent()) :: :ok
|
||||
def refresh(token_agent) do
|
||||
Agent.update(token_agent, fn state ->
|
||||
get_state_with_new_tokens(state)
|
||||
end)
|
||||
end
|
||||
|
||||
defp get_state_with_new_tokens(
|
||||
%TokenAgent{client_with_token: %Client{token: nil}, initial_client: client} = state
|
||||
) do
|
||||
Logger.info("Refreshing tokens for TokenAgent #{state.name}")
|
||||
|
||||
%TokenAgent{
|
||||
state
|
||||
| client_with_token: Client.get_token!(client),
|
||||
last_refreshed: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
defp get_state_with_new_tokens(
|
||||
%TokenAgent{
|
||||
client_with_token: %Client{token: %AccessToken{refresh_token: nil}},
|
||||
initial_client: client
|
||||
} = state
|
||||
) do
|
||||
Logger.info("Refreshing tokens for TokenAgent #{state.name}")
|
||||
|
||||
%TokenAgent{
|
||||
state
|
||||
| client_with_token: Client.get_token!(client),
|
||||
last_refreshed: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
|
||||
defp get_state_with_new_tokens(
|
||||
%TokenAgent{client_with_token: client_with_token, initial_client: initial_client} =
|
||||
state
|
||||
) do
|
||||
Logger.info("Refreshing tokens for TokenAgent #{state.name}")
|
||||
|
||||
case Client.refresh_token(client_with_token) do
|
||||
{:ok, client} ->
|
||||
%TokenAgent{state | client_with_token: client, last_refreshed: DateTime.utc_now()}
|
||||
|
||||
{:error, error} ->
|
||||
Logger.warning(
|
||||
"Unable to use refresh token for TokenAgent #{state.name}: #{inspect(error)}; attempting to obtain new tokens using the initial client"
|
||||
)
|
||||
|
||||
%TokenAgent{
|
||||
state
|
||||
| client_with_token: Client.get_token!(initial_client),
|
||||
last_refreshed: DateTime.utc_now()
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
35
lib/o_auth2_token_manager/token_refresh_strategy.ex
Normal file
35
lib/o_auth2_token_manager/token_refresh_strategy.ex
Normal file
@ -0,0 +1,35 @@
|
||||
defmodule OAuth2TokenManager.TokenRefreshStrategy do
|
||||
@moduledoc """
|
||||
Module defining a struct for representing strategies for refreshing tokens and
|
||||
the functions for applying them
|
||||
"""
|
||||
|
||||
@typedoc """
|
||||
Struct for the refresh timing strategy for OAuth2 tokens; multiple mechanisms
|
||||
|
||||
## Fields
|
||||
* `:every_seconds` - refresh the token if at least the specified number of seconds has elapsed since the last refresh
|
||||
* `:seconds_before_expires` - refresh the token if it will expire within the specified number of seconds
|
||||
"""
|
||||
|
||||
use TypedStruct
|
||||
|
||||
alias __MODULE__
|
||||
|
||||
typedstruct do
|
||||
field(:every_seconds, integer())
|
||||
field(:seconds_before_expires, integer())
|
||||
end
|
||||
|
||||
@doc """
|
||||
Returns true if at least one of the conditions is met and the token should be refreshed
|
||||
"""
|
||||
@spec refresh_now?(TokenRefreshStrategy.t(), Calendar.datetime(), Calendar.datetime()) ::
|
||||
boolean()
|
||||
def refresh_now?(strategy, lastRefreshed, expiresAt) do
|
||||
(strategy.every_seconds &&
|
||||
strategy.every_seconds <= DateTime.diff(DateTime.utc_now(), lastRefreshed)) ||
|
||||
(strategy.seconds_before_expires && expiresAt &&
|
||||
strategy.seconds_before_expires >= DateTime.diff(expiresAt, DateTime.utc_now()))
|
||||
end
|
||||
end
|
||||
41
mix.exs
Normal file
41
mix.exs
Normal file
@ -0,0 +1,41 @@
|
||||
defmodule OAuth2TokenManager.MixProject do
|
||||
use Mix.Project
|
||||
|
||||
def project do
|
||||
[
|
||||
aliases: aliases(),
|
||||
app: :oauth2_token_manager,
|
||||
version: "0.1.0",
|
||||
build_path: "../../_build",
|
||||
config_path: "../../config/config.exs",
|
||||
deps_path: "../../deps",
|
||||
lockfile: "../../mix.lock",
|
||||
elixir: "~> 1.15",
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
extra_applications: [:logger]
|
||||
]
|
||||
end
|
||||
|
||||
# Run "mix help deps" to learn about dependencies.
|
||||
defp deps do
|
||||
[
|
||||
{:oauth2, "== 2.1.0"},
|
||||
{:typed_struct, "== 0.3.0"},
|
||||
{:jason, "== 1.4.1"},
|
||||
{:mock, "== 0.3.8", only: :test}
|
||||
]
|
||||
end
|
||||
|
||||
defp aliases do
|
||||
[
|
||||
setup: []
|
||||
]
|
||||
end
|
||||
end
|
||||
309
test/o_auth2_token_manager/token_agent_test.exs
Normal file
309
test/o_auth2_token_manager/token_agent_test.exs
Normal file
@ -0,0 +1,309 @@
|
||||
defmodule OAuth2TokenManager.TokenAgentTest do
|
||||
@moduledoc """
|
||||
Tests for TokenAgent, which is used to track token state
|
||||
"""
|
||||
use ExUnit.Case, async: false
|
||||
|
||||
import Mock
|
||||
|
||||
alias OAuth2.{AccessToken, Client}
|
||||
alias OAuth2TokenManager.{TokenAgent, TokenRefreshStrategy}
|
||||
|
||||
defp test_client do
|
||||
Client.new(
|
||||
strategy: OAuth2.Strategy.ClientCredentials,
|
||||
client_id: "test_client_id",
|
||||
client_secret: "test_client_secret_abc123",
|
||||
site: "http://localhost/"
|
||||
)
|
||||
end
|
||||
|
||||
describe "start_link/1" do
|
||||
test "returns an agent storing the token if successful" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 6000
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, agent} = TokenAgent.start_link(initial_client: test_client())
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "test_access_token"
|
||||
|
||||
assert TokenAgent.get_current_client(agent).token.access_token == "test_access_token"
|
||||
end
|
||||
end
|
||||
|
||||
test "returns an error if the initial token retrieval did not succeed" do
|
||||
sample_error = %OAuth2.Error{reason: :econnrefused}
|
||||
|
||||
with_mock Client, [:passthrough], get_token: fn _client -> {:error, sample_error} end do
|
||||
{:error, error} = TokenAgent.start_link(initial_client: test_client())
|
||||
|
||||
assert error == sample_error
|
||||
end
|
||||
end
|
||||
|
||||
test "returns an error if no client is provided" do
|
||||
{:error, ":initial_client required"} = TokenAgent.start_link(name: MyModule.TokenAgent)
|
||||
end
|
||||
|
||||
test "can be provided with an name to refer to the agent" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 6000
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, _} =
|
||||
TokenAgent.start_link(
|
||||
initial_client: test_client(),
|
||||
inline_refresh_strategy: %TokenRefreshStrategy{},
|
||||
name: MyModule.TokenAgent
|
||||
)
|
||||
|
||||
assert TokenAgent.get_access_token(MyModule.TokenAgent) == "test_access_token"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "refresh_tokens/1" do
|
||||
test "uses the refresh token if available" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600,
|
||||
refresh_token: "test_refresh_token"
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600,
|
||||
refresh_token: "new_refresh_token"
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, agent} = TokenAgent.start_link(initial_client: test_client())
|
||||
TokenAgent.refresh(agent)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "new_access_token"
|
||||
end
|
||||
end
|
||||
|
||||
test "uses the initial client to get a new token if the refresh token cannot be used" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600,
|
||||
refresh_token: "test_refresh_token"
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn _client -> {:error, %OAuth2.Error{reason: :econnrefused}} end,
|
||||
get_token!: fn client ->
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600,
|
||||
refresh_token: "new_refresh_token"
|
||||
}
|
||||
}
|
||||
end do
|
||||
{:ok, agent} = TokenAgent.start_link(initial_client: test_client())
|
||||
TokenAgent.refresh(agent)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "new_access_token"
|
||||
|
||||
assert_called(Client.refresh_token(:_))
|
||||
end
|
||||
end
|
||||
|
||||
test "uses the initial client to get a new token if no refresh token is available" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn _client -> {:error, %OAuth2.Error{reason: :econnrefused}} end,
|
||||
get_token!: fn client ->
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600
|
||||
}
|
||||
}
|
||||
end do
|
||||
{:ok, agent} = TokenAgent.start_link(initial_client: test_client())
|
||||
TokenAgent.refresh(agent)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "new_access_token"
|
||||
|
||||
assert_not_called(Client.refresh_token(:_))
|
||||
end
|
||||
end
|
||||
|
||||
test "uses the initial client to get a new token if no token is available" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok, client}
|
||||
end,
|
||||
refresh_token: fn _client -> {:error, %OAuth2.Error{reason: :econnrefused}} end,
|
||||
get_token!: fn client ->
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 600
|
||||
}
|
||||
}
|
||||
end do
|
||||
{:ok, agent} = TokenAgent.start_link(initial_client: test_client())
|
||||
TokenAgent.refresh(agent)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "new_access_token"
|
||||
|
||||
assert_not_called(Client.refresh_token(:_))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "inline refresh" do
|
||||
test "triggers during get_access_token if the inline_refresh strategy requires it" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 10,
|
||||
refresh_token: "test_refresh_token"
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 6000,
|
||||
refresh_token: "new_refresh_token"
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, agent} =
|
||||
TokenAgent.start_link(
|
||||
initial_client: test_client(),
|
||||
inline_refresh_strategy: %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30
|
||||
}
|
||||
)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "new_access_token"
|
||||
end
|
||||
end
|
||||
|
||||
test "does not trigger during get_access_token if the inline_refresh strategy does not require it" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 100,
|
||||
refresh_token: "test_refresh_token"
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 6000,
|
||||
refresh_token: "new_refresh_token"
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, agent} =
|
||||
TokenAgent.start_link(
|
||||
initial_client: test_client(),
|
||||
inline_refresh_strategy: %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30
|
||||
}
|
||||
)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "test_access_token"
|
||||
|
||||
assert_not_called(Client.refresh_token(:_))
|
||||
end
|
||||
end
|
||||
|
||||
test "does not trigger during get_access_token if no inline_refresh strategy is set" do
|
||||
with_mock Client, [:passthrough],
|
||||
get_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "test_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 10,
|
||||
refresh_token: "test_refresh_token"
|
||||
}
|
||||
}}
|
||||
end,
|
||||
refresh_token: fn client ->
|
||||
{:ok,
|
||||
%Client{
|
||||
client
|
||||
| token: %AccessToken{
|
||||
access_token: "new_access_token",
|
||||
expires_at: (DateTime.utc_now() |> DateTime.to_unix()) + 6000,
|
||||
refresh_token: "new_refresh_token"
|
||||
}
|
||||
}}
|
||||
end do
|
||||
{:ok, agent} =
|
||||
TokenAgent.start_link(initial_client: test_client(), inline_refresh_strategy: nil)
|
||||
|
||||
assert TokenAgent.get_access_token(agent) == "test_access_token"
|
||||
|
||||
assert_not_called(Client.refresh_token(:_))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
108
test/o_auth2_token_manager/token_refresh_strategy_test.exs
Normal file
108
test/o_auth2_token_manager/token_refresh_strategy_test.exs
Normal file
@ -0,0 +1,108 @@
|
||||
defmodule OAuth2TokenManager.TokenRefreshStrategyTest do
|
||||
@moduledoc """
|
||||
Tests the refresh strategies configured with TokenRefreshStrategy
|
||||
"""
|
||||
|
||||
use ExUnit.Case
|
||||
|
||||
alias OAuth2TokenManager.TokenRefreshStrategy
|
||||
|
||||
defp seconds_from_now(seconds) do
|
||||
DateTime.utc_now() |> DateTime.add(seconds, :second, Calendar.UTCOnlyTimeZoneDatabase)
|
||||
end
|
||||
|
||||
describe "every_seconds" do
|
||||
test "refresh_now? is true if it has been more than the configured value of seconds since the last refresh" do
|
||||
strategy = %TokenRefreshStrategy{
|
||||
every_seconds: 30
|
||||
}
|
||||
|
||||
assert !TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
DateTime.utc_now(),
|
||||
seconds_from_now(300)
|
||||
)
|
||||
|
||||
assert TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
seconds_from_now(300)
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "seconds_before_expires" do
|
||||
test "refresh_now? is true if if the token expires at or before the specified number of seconds" do
|
||||
strategy = %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30
|
||||
}
|
||||
|
||||
assert !TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
seconds_from_now(60)
|
||||
)
|
||||
|
||||
assert TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
seconds_from_now(30)
|
||||
)
|
||||
end
|
||||
|
||||
test "refresh_now is false if expires_at is nil" do
|
||||
strategy = %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30
|
||||
}
|
||||
|
||||
assert !TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
nil
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
describe "refresh_now?" do
|
||||
test "returns true if any of the conditions in the strategy are met" do
|
||||
strategy = %TokenRefreshStrategy{
|
||||
seconds_before_expires: 30,
|
||||
every_seconds: 60
|
||||
}
|
||||
|
||||
assert !TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
seconds_from_now(60)
|
||||
)
|
||||
|
||||
assert TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-60),
|
||||
seconds_from_now(30)
|
||||
)
|
||||
|
||||
assert TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-30),
|
||||
seconds_from_now(30)
|
||||
)
|
||||
|
||||
assert TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-60),
|
||||
seconds_from_now(60)
|
||||
)
|
||||
end
|
||||
|
||||
test "returns false if none of the conditions in the strategy are specified" do
|
||||
strategy = %TokenRefreshStrategy{}
|
||||
|
||||
assert !TokenRefreshStrategy.refresh_now?(
|
||||
strategy,
|
||||
seconds_from_now(-1),
|
||||
seconds_from_now(1)
|
||||
)
|
||||
end
|
||||
end
|
||||
end
|
||||
1
test/test_helper.exs
Normal file
1
test/test_helper.exs
Normal file
@ -0,0 +1 @@
|
||||
ExUnit.start()
|
||||
Loading…
Reference in New Issue
Block a user