stevenmaguire/oauth2-dropbox
A Dropbox OAuth 2.0 provider for the League OAuth 2.0 client. Handles Dropbox authorization URLs, access token exchange, refresh tokens, and resource owner details, so PHP apps can add "Sign in with Dropbox" quickly.
Install the provider alongside the League OAuth 2.0 client (Composer pulls it in):
composer require stevenmaguire/oauth2-dropbox
Before writing code, create a Dropbox app in the App Console:
account_info.read, files.metadata.read).Minimal server-side flow:
use StevenMaguire\OAuth2\Client\Provider\Dropbox;
$provider = new Dropbox([
'clientId' => config('services.dropbox.client_id'),
'clientSecret' => config('services.dropbox.client_secret'),
'redirectUri' => route('dropbox.callback'),
]);
// Redirect the user
$authUrl = $provider->getAuthorizationUrl([
'scope' => ['account_info.read', 'files.metadata.read'],
'token_access_type' => 'offline', // needed for a refresh token
]);
session(['dropbox_oauth2state' => $provider->getState()]);
return redirect($authUrl);
// Handle the callback
if (empty($_GET['state']) || $_GET['state'] !== session('dropbox_oauth2state')) {
abort(403, 'Invalid OAuth state');
}
$token = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code'],
]);
$owner = $provider->getResourceOwner($token);
$owner->getId(); // Dropbox account_id
$owner->getName(); // display name
$owner->getEmail(); // may be null depending on scopes
The first use case is “Sign in with Dropbox”: exchange the code, persist the token, call users/get_current_account through getResourceOwner(), and key your local user record on the returned account_id.
Where to look first:
StevenMaguire\OAuth2\Client\Provider\Dropbox — URLs, scopes, error handling, resource-owner creation.StevenMaguire\OAuth2\Client\Provider\DropboxResourceOwner — which fields are exposed.AbstractProvider, AccessToken, and getAuthenticatedRequest() docs — most day-to-day API work happens there, not in this package.Bind the provider per request, not as a long-lived singleton. The provider carries OAuth state. A singleton is usually fine under PHP-FPM because the container dies with the request, but under Octane or queue workers you should use a request-scoped binding or instantiate it in the controller.
$this->app->bind(Dropbox::class, fn () => new Dropbox([
'clientId' => config('services.dropbox.client_id'),
'clientSecret' => config('services.dropbox.client_secret'),
'redirectUri' => config('services.dropbox.redirect_uri'),
]));
Persist the full token, encrypted. Store access_token, refresh_token, expiry, and the Dropbox account_id. Use Laravel’s encrypted cast; a bearer token is a credential.
$row->update([
'access_token' => $token->getToken(),
'refresh_token' => $token->getRefreshToken() ?? $row->refresh_token,
'expires_at' => $token->getExpires(),
'dropbox_account_id'=> $owner->getId(),
]);
Rehydrate later with the League token object:
$token = new \League\OAuth2\Client\Token\AccessToken([
'access_token' => $row->access_token,
'refresh_token' => $row->refresh_token,
'expires' => $row->expires_at?->timestamp,
]);
Refresh proactively. Dropbox access tokens are short-lived (about four hours). Refresh before expiry rather than reacting to a 401:
if ($token->getExpires() && $token->getExpires() < time() + 60) {
if (! $token->getRefreshToken()) {
// force re-authorization
}
$new = $provider->getAccessToken('refresh_token', [
'refresh_token' => $token->getRefreshToken(),
]);
// Dropbox may omit a new refresh token; keep the old one.
$row->update([
'access_token' => $new->getToken(),
'refresh_token' => $new->getRefreshToken() ?? $token->getRefreshToken(),
'expires_at' => $new->getExpires(),
]);
}
Use getAuthenticatedRequest() for occasional Dropbox API calls. It applies the bearer header and lets you control method, headers, and body:
$request = $provider->getAuthenticatedRequest(
'POST',
'https://api.dropboxapi.com/2/files/list_folder',
$token,
[
'headers' => ['Content-Type' => 'application/json'],
'body' => json_encode(['path' => '', 'recursive' => false]),
]
);
$data = $provider->getParsedResponse($request);
For heavier Dropbox usage, hand $token->getToken() to dropbox/dropbox-sdk-php and let the SDK handle typed endpoints; keep this provider for the OAuth handshake and identity lookup.
Centralize scopes in config. Scope changes force users to re-consent, so treat the list as part of your app contract. Request the minimum: account_info.read for identity, files.metadata.read for listing, files.content.read only when downloading.
Team apps are manual. If the token response includes team_id in getValues(), you must add the Dropbox-API-Select-User header yourself via the headers option on getAuthenticatedRequest(); the provider does not model team admin flows.
Testing. Inject a mocked Guzzle client with $provider->setHttpClient($client) and assert the generated authorization URL and token request. For feature tests, skip the network and insert an encrypted token row, then test your refresh and API-calling code in isolation.
Laravel Socialite. There is no official Dropbox driver. Either write a small custom Socialite provider, or, more simply, skip Socialite and use this provider directly in a controller. Do not mix Socialite’s flow with this provider’s state handling; you will end up with two competing state values.
users/get_current_account is POST-only. The provider accounts for this in fetchResourceOwnerDetails(); if you subclass and override that method, preserve the POST and the api.dropboxapi.com host or you will get 405/400s.getAuthorizationUrl() without scope, Dropbox issues a token with no permissions and every API call fails. Always pass an explicit scope array. Scopes must also be enabled in the App Console, otherwise the consent screen errors before your callback ever runs.token_access_type=offline is required for refresh tokens. Without it you get a short-lived access token and no refresh token. It is passed as an option to getAuthorizationUrl() and survives into the query string.null; fall back to the previous value.http vs https, and ports all matter. Dropbox expects HTTPS for production; use a tunnel for local development if localhost is rejected.state yourself. The provider generates it but does not verify it on callback. Compare against your session and reject mismatches.getId() is Dropbox’s account_id. There is no numeric id field. Use account_id as your stable external key.Content-Type: application/json and json_encode() the body yourself. Also remember Dropbox root is "", not "/".error and error_description; API errors use structured JSON with error_summary. IdentityProviderException::getResponseBody() is your friend when debugging.Retry-After. The provider does not retry; wrap calls with backoff or use Http::retry() if you bypass the provider.https://api.dropboxapi.com/2/auth/token/revoke with the bearer token when a user disconnects, then clear your stored credentials.league/oauth2-client and verify PHP compatibility in CI. For greenfield projects, the official dropbox/dropbox-sdk-php (WebAuth) is a viable alternative; this package remains the right choice if you are standardizing on League providers.env() inside provider construction. Under config:cache, env() returns null; always read from config('services.dropbox.*').How can I help you explore Laravel packages today?