Effective September 2021, Dropbox will be deprecating long-lived access tokens.
This GIST generally describes how to authenticate requests to Dropbox API v2, for anyone working on a server-side PHP Dropbox implementation.
It's important to understand three types of codes you'll encounter:
- Access Code - this is a one-time code that represents user-granted app access.
- Access Token - this is short-lived token that provides access to Dropbox API endpoints.
- Refresh Token - this is a long-lived token that allows you to fetch a fresh Access Token.
You'll want to obtain a Refresh Token and store that securely as an environment variable on your server. Dropbox Refresh Tokens are long-lived, and do not expire unless explicitly revoked. You should use this Refresh Token to fetch fresh Access Tokens (which are short-lived) as needed. This is the appropriate flow per the Dropbox team as described in their oAuth Guide and by a Dropbox dev in this community thread.
To do that, first obtain an Access Code by visiting the URL noted below. You'll be brought to the Dropbox website to authorize your app, after which you'll be presented with the access code.
Note: you must include a token_access_type
URL parameter, with a value of offline
, otherwise a refresh token wont be included in your initial access request.
https://www.dropbox.com/oauth2/authorize?client_id=<YOUR_APP_KEY>&response_type=code&token_access_type=offline
Once you've obtained your Access Code, you're going to make your first request to obtain an Access Token. I'd recommend just doing this from console as you'll only need to do it once for the purpose of grabbing a Refresh Token.
curl https://api.dropbox.com/oauth2/token -d code=<ACCESS_CODE> -d grant_type=authorization_code -u <APP_KEY>:<APP_SECRET>
You should get back a response that looks something like this -- your scopes will of course be based on what you've setup for your app.
{
"uid": "xxxxxxxxxx",
"access_token": "xxxxxxxxxx",
"expires_in": 14400,
"token_type": "bearer",
"scope": "files.content.read files.content.write",
"refresh_token": "xxxxxxxxxx",
"account_id": "dbid:xxxxxxxxxx"
}
Here, you're looking for the refresh_token
. This is the long-lived token that you'll want to securely store in your server environment. This token will allow you to make repeated requests to /oauth2/token for fresh (short-lived) access tokens needed to interact with Dropbox API endpoints.
You should now use this Refresh Token to fetch your Access Token's as needed. here's an example of how to do that using Guzzle:
private function getToken()
{
try {
$client = new \GuzzleHttp\Client();
$res = $client->request("POST", "https://{$this->key}:{$this->secret}@api.dropbox.com/oauth2/token", [
'form_params' => [
'grant_type' => 'refresh_token',
'refresh_token' => $this->refreshToken,
]
]);
if ($res->getStatusCode() == 200) {
return json_decode($res->getBody(), TRUE);
} else {
return false;
}
}
catch (Exception $e) {
$this->logger->error("[{$e->getCode()}] {$e->getMessage()}");
return false;
}
}
The Dropbox response will look like this:
{
"token_type": "bearer",
"access_token": "xxxxxxxxxx",
"expires_in": 14400
}
Considering a server-side implementation, you'll want to think about storing this token, such that you can avoid making repeated requests on every request within your application or API.
If you had a distributed application/api, an ideal solution might be a cache-aside strategy using Redis. You could cache this token while setting a TTL (expiry). Then, as part of your Dropbox model, you could check if the token exists in your Redis cache. If it exists, use it, otherwise, request a fresh token and then update your Redis cache with that new token.
# note, im subtracting 100 seconds from `expires_in` to add a little buffer room
if ( $redis->exists("dropbox:token") === 0 ) {
$token = $this->getToken()['access_token'];
$redis->set("dropbox:token", $token);
$redis->expireAt("dropbox:token", Carbon::now()->addSeconds(($token['expires_in'] - 100))->timestamp);
} else {
$token = $redis->get("dropbox:token");
}
// process your request to dropbox using $token
Alternatively, you could store the Dropbox access token in a server session, along with a timestamp we can compare for expiration. Although this is not as effective given that sessions are not global (in the context of a distributed application). You could potentially find a scenario where a single user will unnecessarily end up requesting multiple access tokens -- notably true in a load balanced environment where server affinity/sticky sessions is not used. PHP sessions also do not have the convenience of TTL expirations available in Redis.
All the same, here's an example using server sessions:
# note, im subtracting 100 seconds from `expires_in` to add a little buffer room
private function setSessionToken()
{
if(session_status() == PHP_SESSION_NONE) {
session_start();
}
$token = $this->getToken();
$_SESSION['dropboxToken'] = $token['access_token'];
$_SESSION['dropboxExpires'] = Carbon::now()->timestamp + ($token['expires_in'] - 100);
return $token['access_token'];
}
private function getSessionToken()
{
if( isset($_SESSION['dropboxToken']) && !empty($_SESSION['dropboxToken']) ) {
if( Carbon::now()->timestamp < $_SESSION['dropboxExpires'] ) {
return $_SESSION['dropboxToken'];
}
unset($_SESSION["dropboxToken"]);
unset($_SESSION["dropboxExpires"]);
}
return $this->setSessionToken();
}
$token = $this->getSessionToken();
// process your request to dropbox using $token
Lastly, once you have your access token, you can make requests to the Dropbox API. Here's an example for listing assets:
public function listAssets(string $path = '')
{
try {
$client = new \GuzzleHttp\Client();
$res = $client->request("POST", "https://api.dropboxapi.com/2/files/list_folder", [
'headers' => [
'authorization' => "Bearer {$this->getSessionToken()}",
],
'json' => [
'path' => $path,
'recursive' => false,
'include_deleted' => false,
'include_has_explicit_shared_members' => false,
'include_mounted_folders' => true,
'include_non_downloadable_files' => true
]
]);
if ($res->getStatusCode() == 200) {
return json_decode($res->getBody(), TRUE);
}
return false;
}
catch (Exception $e) {
$this->logger->error("[{$e->getCode()}] {$e->getMessage()}");
return false;
}
}