Magpie.Auth (Magpie v0.3.1)

Copy Markdown View Source

OAuth 2 flow helpers — plus the endpoints of the Dropbox auth namespace.

Dropbox no longer issues long-lived access tokens: they expire after about four hours, so any application that runs longer than that must use the refresh token flow. The functions here are stateless — they build the authorization URL and talk to Dropbox's /oauth2/token endpoint. Keeping a token fresh over time is Magpie.Auth.TokenServer's job.

The whole flow, end to end:

# 1. Send the user to Dropbox
{verifier, challenge} = Magpie.Auth.pkce_pair()

url =
  Magpie.Auth.authorize_url(app_key,
    redirect_uri: "https://myapp.com/dropbox/callback",
    state: csrf_token,
    scope: ["account_info.read", "files.content.write"],
    code_challenge: challenge
  )

# 2. Dropbox redirects back with ?code=...
{:ok, token} =
  Magpie.Auth.exchange_code(app_key, code,
    code_verifier: verifier,
    redirect_uri: "https://myapp.com/dropbox/callback"
  )

# 3. Store token.refresh_token, then build clients from it
client = Magpie.Client.new(refresh_token: token.refresh_token, app_key: app_key, pkce: true)

See the OAuth guide for the full walkthrough, including how to get a refresh token without writing any code.

Endpoints

The OAuth endpoints can be overridden (rarely needed) via:

config :magpie,
  oauth_authorize_url: "https://www.dropbox.com/oauth2/authorize",
  oauth_token_url: "https://api.dropboxapi.com/oauth2/token"

Summary

Functions

Builds the URL where the user authorizes your app.

Exchanges the authorization code Dropbox sent to your redirect URI for a token set.

Derives the S256 PKCE challenge of a verifier.

Generates a PKCE {code_verifier, code_challenge} pair.

Trades a refresh token for a fresh access token.

Disables the access token used to authenticate the call.

Types

token_opt()

@type token_opt() ::
  {:app_secret, String.t() | nil}
  | {:code_verifier, String.t() | nil}
  | {:redirect_uri, String.t() | nil}

Functions

authorize_url(app_key, opts \\ [])

@spec authorize_url(
  String.t(),
  keyword()
) :: String.t()

Builds the URL where the user authorizes your app.

token_access_type defaults to "offline", which is what makes Dropbox return a refresh token alongside the access token.

Options

  • :redirect_uri — where Dropbox sends the user back to. Must match one of the redirect URIs registered in the App Console
  • :state — opaque value echoed back in the redirect, used to protect against CSRF (and to carry your own context)
  • :scope — list of scopes (or an already space-separated string). When omitted, the app's configured scopes are granted
  • :code_challenge — PKCE challenge from pkce_pair/0; adds code_challenge_method=S256
  • :token_access_type"offline" (default), "online" or "legacy"
  • :extra_params — keyword list or map of additional Dropbox authorization params (force_reapprove, locale, require_role, disable_signup, ...), appended after the standard ones. Values are rendered with to_string/1, so booleans and atoms work. Params this function already sets cannot be overridden — a collision raises ArgumentError

Examples

iex> Magpie.Auth.authorize_url("APP_KEY")
"https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&response_type=code&token_access_type=offline"

iex> Magpie.Auth.authorize_url("APP_KEY",
...>   redirect_uri: "https://myapp.com/cb",
...>   scope: ["files.content.read", "files.content.write"]
...> )
"https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&response_type=code&token_access_type=offline&redirect_uri=https%3A%2F%2Fmyapp.com%2Fcb&scope=files.content.read%20files.content.write"

iex> Magpie.Auth.authorize_url("APP_KEY",
...>   extra_params: [force_reapprove: true, locale: "pt_BR"]
...> )
"https://www.dropbox.com/oauth2/authorize?client_id=APP_KEY&response_type=code&token_access_type=offline&force_reapprove=true&locale=pt_BR"

exchange_code(app_key, code, opts \\ [])

@spec exchange_code(String.t(), String.t(), [token_opt()]) ::
  {:ok, Magpie.Auth.Token.t()} | {:error, Magpie.Error.t()}

Exchanges the authorization code Dropbox sent to your redirect URI for a token set.

Pass :app_secret for confidential apps, or :code_verifier for PKCE apps. :redirect_uri is required whenever it was present in the authorization request, and must be identical.

This is the only response that carries a refresh_token — store it.

{:ok, %Magpie.Auth.Token{refresh_token: refresh_token}} =
  Magpie.Auth.exchange_code(app_key, code,
    app_secret: app_secret,
    redirect_uri: "https://myapp.com/dropbox/callback"
  )

Returns {:error, %Magpie.Error{}} when Dropbox rejects the exchange — summary then holds the OAuth error code (e.g. "invalid_grant" for an expired or already-used code).

pkce_challenge(verifier)

@spec pkce_challenge(String.t()) :: String.t()

Derives the S256 PKCE challenge of a verifier.

Test vector from RFC 7636, appendix B:

iex> Magpie.Auth.pkce_challenge("dBjftJeZ4CVP-mB92K27uhbUJU1p1r_wW1gFWFOEjXk")
"E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM"

pkce_pair()

@spec pkce_pair() :: {verifier :: String.t(), challenge :: String.t()}

Generates a PKCE {code_verifier, code_challenge} pair.

Public apps (mobile, desktop, CLIs — anything that cannot keep a secret) use PKCE instead of the app secret: send the challenge to authorize_url/2 and the verifier to exchange_code/3.

iex> {verifier, challenge} = Magpie.Auth.pkce_pair()
iex> byte_size(verifier)
86
iex> challenge == Magpie.Auth.pkce_challenge(verifier)
true

refresh(app_key, refresh_token, opts \\ [])

@spec refresh(String.t(), String.t(), [token_opt()]) ::
  {:ok, Magpie.Auth.Token.t()} | {:error, Magpie.Error.t()}

Trades a refresh token for a fresh access token.

Pass :app_secret for confidential apps; PKCE apps authenticate with the app key alone. Dropbox does not rotate refresh tokens, so the response carries no refresh_token — keep using the one you already have.

{:ok, %Magpie.Auth.Token{access_token: access_token, expires_at: expires_at}} =
  Magpie.Auth.refresh(app_key, refresh_token, app_secret: app_secret)

Returns {:error, %Magpie.Error{}} when the refresh token was revoked or is invalid (summary: "invalid_grant").

token_revoke(client)

Disables the access token used to authenticate the call.

More info at: https://www.dropbox.com/developers/documentation/http/documentation#auth-token-revoke