Magpie.Auth.TokenServer (Magpie v0.3.1)

Copy Markdown View Source

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 for set_refresh_token/3
  • :app_secret — your app secret. Required unless pkce: true
  • :pkce — set to true for public apps, which authenticate with the app key alone and therefore have no secret
  • :name — registered name, so Magpie.Client.new/1 and 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 (default 300)
  • :on_refresh — 1-arity function called with the new Magpie.Auth.Token after 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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

fetch_token(server)

@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)

refresh_token(server)

@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.

set_refresh_token(server, refresh_token, opts \\ [])

@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 a DateTime

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
  )

start_link(opts)

@spec start_link(keyword()) :: GenServer.on_start()

Starts the server.

See the module documentation for the supported options.

token(server)

@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.