A Magpie.Auth.TokenProvider that keeps an access token fresh.
Holds the current Magpie.Auth.Token plus the credentials needed to
renew it, and refreshes it a few minutes before it expires — so requests
never wait on a token that Dropbox is about to reject.
Put it in your supervision tree and hand its name to the client:
children = [
{Magpie.Auth.TokenServer,
name: MyApp.DropboxToken,
app_key: System.fetch_env!("DROPBOX_APP_KEY"),
app_secret: System.fetch_env!("DROPBOX_APP_SECRET"),
refresh_token: System.fetch_env!("DROPBOX_REFRESH_TOKEN")}
]
client = Magpie.Client.new(token_provider: {Magpie.Auth.TokenServer, MyApp.DropboxToken})The refresh token itself is optional at startup. A server started without
one sits in an unconfigured state — calls return
{:error, %Magpie.Error{summary: "no_refresh_token"}} until
set_refresh_token/3 hands it a token. That makes a plain static
supervision tree work even on a fresh install, where the user has not
authorized the app yet; see the OAuth guide.
Options
:app_key— your Dropbox app key (required):refresh_token— the long-lived refresh token. Optional: without it the server starts unconfigured, waiting forset_refresh_token/3:app_secret— your app secret. Required unlesspkce: true:pkce— set totruefor public apps, which authenticate with the app key alone and therefore have no secret:name— registered name, soMagpie.Client.new/1and your supervision tree can refer to the server without carrying its pid:access_token/:expires_at— an access token you already have, to avoid a refresh on the first call. Pass both or neither: a token without a known expiry is refreshed right away:refresh_margin— how many seconds before expiry a token is considered stale (default300):on_refresh— 1-arity function called with the newMagpie.Auth.Tokenafter every successful refresh, e.g. to persist it
Concurrency
Refreshes happen inside the server, so concurrent callers queue behind a
single HTTP request — a burst of requests can never trigger a burst of
refreshes. When a refresh fails the server keeps its previous state and
replies {:error, %Magpie.Error{}} to the caller instead of crashing:
a temporarily unreachable Dropbox should not take your supervision tree
down, and the next call simply tries again.
set_refresh_token/3 goes through the same serialization: it runs before
or after a refresh, never during one, so a refresh finishing around a
re-authorization can never resurrect the old token.
Summary
Functions
Returns a specification to start this module under a supervisor.
Returns the current access token, refreshing it first when it is expired
or within :refresh_margin seconds of expiring.
Forces a refresh and returns the new access token.
Stores a (new) refresh token, configuring the server or replacing the token it holds — the re-authorization case.
Starts the server.
Returns the whole token currently held by the server, without refreshing.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec fetch_token(GenServer.server()) :: {:ok, String.t()} | {:error, Magpie.Error.t()}
Returns the current access token, refreshing it first when it is expired
or within :refresh_margin seconds of expiring.
{:ok, access_token} = Magpie.Auth.TokenServer.fetch_token(MyApp.DropboxToken)An unconfigured server (no refresh token yet) answers with a pattern-matchable error instead of calling Dropbox:
{:error, %Magpie.Error{summary: "no_refresh_token"}} =
Magpie.Auth.TokenServer.fetch_token(MyApp.DropboxToken)
@spec refresh_token(GenServer.server()) :: {:ok, String.t()} | {:error, Magpie.Error.t()}
Forces a refresh and returns the new access token.
Magpie calls this for you when Dropbox rejects a token as expired; you rarely need to call it yourself.
@spec set_refresh_token(GenServer.server(), String.t(), keyword()) :: :ok
Stores a (new) refresh token, configuring the server or replacing the token it holds — the re-authorization case.
Any cached access token is discarded, since it belongs to the previous authorization. If you already hold a valid pair, seed the cache to save the first refresh — pass both options or neither:
:access_token— a currently valid access token:expires_at— its absolute expiry, as aDateTime
Dropbox is not contacted: the next fetch_token/1 performs the refresh
(or uses the seeded pair), so this is safe to call from a web request.
# OAuth callback, right after the code exchange
{:ok, token} = Magpie.Auth.exchange_code(app_key, code, app_secret: secret)
:ok = Magpie.Auth.TokenServer.set_refresh_token(MyApp.DropboxToken, token.refresh_token)
# On boot, from storage — seeded, so no refresh until it nears expiry
:ok =
Magpie.Auth.TokenServer.set_refresh_token(MyApp.DropboxToken, stored.refresh_token,
access_token: stored.access_token,
expires_at: stored.expires_at
)
@spec start_link(keyword()) :: GenServer.on_start()
Starts the server.
See the module documentation for the supported options.
@spec token(GenServer.server()) :: Magpie.Auth.Token.t() | nil
Returns the whole token currently held by the server, without refreshing.
Handy for inspecting the expiry or persisting the token on shutdown.
Returns nil while the server is unconfigured.