Skip to content

Commit 30245ab

Browse files
committed
Add refresh token authentication
1 parent f83e517 commit 30245ab

File tree

11 files changed

+551
-52
lines changed

11 files changed

+551
-52
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
.idea
22
*.cache
3+
/vendor

CHANGELOG.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [1.1.0] - 2025-07-13
9+
### Changes
10+
- Implemented **OAuth2 Refresh Token Flow** for Dropbox authentication.
11+
- Provides a more secure and persistent authentication method.
12+
- Eliminates the need for manual token renewal every 4 hours.
13+
- Includes a new console command (`php artisan dropboxadapter:setup`) to generate and manage refresh tokens.
14+
- Introduced **Temporary Token Flow** for quick setup and development environments.
15+
- Allows direct use of short-lived access tokens for testing.
16+
- Enhanced `.env` configuration to support both authentication modes via `DROPBOX_AUTH_MODE`.
17+
818
## [1.0.0] - 2025-07-08
919
### Added
1020
- Initial release of the **DropboxAdapter Plugin**.
@@ -15,7 +25,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1525

1626
### Notes
1727
- **Not compatible** with `media` or `uploads` disks in Winter CMS.
18-
- Intended for developer-oriented scenarios such as automation, deployment, or remote syncnot general file management.
28+
- Intended for developer-oriented scenarios such as automation, deployment, or remote syncnot general file management.
1929

2030
---
2131

Plugin.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<?php namespace NumenCode\DropboxAdapter;
22

33
use System\Classes\PluginBase;
4+
use NumenCode\DropboxAdapter\Console\DropboxSetupCommand;
45
use NumenCode\DropboxAdapter\Providers\DropboxServiceProvider;
56

67
class Plugin extends PluginBase
@@ -20,4 +21,14 @@ public function boot()
2021
{
2122
$this->app->register(DropboxServiceProvider::class);
2223
}
24+
25+
public function register()
26+
{
27+
$this->registerConsoleCommands();
28+
}
29+
30+
protected function registerConsoleCommands()
31+
{
32+
$this->registerConsoleCommand('numencode.dropboxadapter_setup', DropboxSetupCommand::class);
33+
}
2334
}

README.md

Lines changed: 106 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Dropbox Adapter Plugin
22

33
The **Dropbox Adapter Plugin** provides Dropbox v2 API integration with Winter CMS as a custom filesystem disk driver.
4-
This allows limited usage of Dropbox with Laravel's Storage facadeprimarily for custom logic and backup tools such as
4+
This allows limited usage of Dropbox with Laravel's Storage facadeprimarily for custom logic and backup tools such as
55
[`NumenCode.SyncOps`](https://github.com/numencode/wn-syncops-plugin).
66

77
[![Version](https://img.shields.io/github/v/release/numencode/wn-dropboxadapter-plugin?style=flat-square&color=0099FF)](https://github.com/numencode/wn-dropboxadapter-plugin/releases)
@@ -21,6 +21,8 @@ Laravel’s filesystem abstraction, especially for automation, remote syncing, a
2121
> filesystem for core `media` or `uploads` disks. This plugin is not intended for direct media asset management,
2222
> but rather for use cases like backup transport, cloud storage sync, or custom plugin integration (e.g. [`NumenCode.SyncOps`](https://github.com/numencode/wn-syncops-plugin)).
2323
24+
---
25+
2426
## Installation
2527

2628
This plugin is available for installation via [Composer](http://getcomposer.org/).
@@ -37,38 +39,123 @@ php artisan winter:up
3739

3840
## Requirements
3941

40-
* [Winter CMS](https://wintercms.com/) version 1.2.7 or newer
41-
* PHP 8.0 or later
42-
* A Dropbox API access token
42+
- [Winter CMS](https://wintercms.com) version 1.2.7 or newer
43+
- PHP 8.0 or later
44+
- A Dropbox App (created in the [Dropbox App Console](https://www.dropbox.com/developers/apps))
45+
- For **Refresh Token Flow**: You'll need your `App key` and `App secret`.
46+
- For **Temporary Token Flow**: You'll need a manually generated `Access token`.
4347

44-
## Configuration
48+
## Configuration and Authentication
4549

46-
1. Create a Dropbox App in the [Dropbox App Console](https://www.dropbox.com/developers/apps) and generate an access token.
47-
2. Define a new filesystem disk in your `config/filesystems.php` file:
48-
```php
49-
'dropbox' => [
50-
'driver' => 'dropbox',
51-
'authorization_token' => env('DROPBOX_AUTH_TOKEN'),
52-
],
53-
```
54-
3. Add the token to your `.env` file:
50+
This plugin offers two methods for authenticating with the Dropbox API: the **Refresh Token Flow** (recommended for
51+
production and long-term use) and the **Temporary Token Flow** (ideal for quick development or testing).
52+
You can switch between these modes in your `.env` file.
53+
54+
### 1. Create a Dropbox App
55+
56+
Regardless of the authentication method, you need to [create a Dropbox App](https://www.dropbox.com/developers/apps).
57+
58+
When creating your app:
59+
60+
- Choose **"Scoped access"** and select the permissions your application needs (e.g.,
61+
`files.content.write`, `files.content.read` for read/write access).
62+
63+
### 2. Choose your authentication method
64+
65+
You specify the authentication method in your `.env` file using the `DROPBOX_AUTH_MODE` variable.
66+
You can choose between `refresh_token` and `temp_token` modes.
67+
68+
#### a. Refresh Token Flow (recommended for production)
69+
70+
This is the **secure and persistent** way to authenticate. It uses a long-lived **refresh token** to automatically
71+
obtain new, short-lived **access tokens** as needed, eliminating the need for manual intervention when tokens expire.
72+
73+
###### Setup Steps:
74+
75+
1. **Add Dropbox App credentials to `.env`**:
76+
77+
You'll need your Dropbox `App key` and `App secret`.
5578
```dotenv
56-
DROPBOX_AUTH_TOKEN=your_generated_token
79+
DROPBOX_AUTH_MODE=refresh_token
80+
DROPBOX_APP_KEY=
81+
DROPBOX_APP_SECRET=
82+
DROPBOX_REFRESH_TOKEN=
83+
```
84+
85+
2. **Generate your refresh token**:
86+
87+
Use the provided console command to go through the OAuth2 authorization process and obtain your refresh token:
88+
```bash
89+
php artisan dropboxadapter:setup
5790
```
58-
You can now interact with Dropbox programmatically using the Storage facade in Laravel:
59-
```php
60-
Storage::disk('dropbox')->put('backups/site.zip', $contents);
91+
92+
Follow the on-screen prompts:
93+
- It will ask for your **Dropbox App key**.
94+
- It will provide a URL to open in your browser. Authorize your app on Dropbox. After authorization, Dropbox will
95+
provide an "authorization code" (often in the URL parameters if no redirect URI is set, or directly on the
96+
success page). Copy this `code`.
97+
- Paste the `authorization code` back into the console.
98+
- Enter your **Dropbox App secret**.
99+
- The command will then exchange this code for a refresh token and display it.
100+
101+
3. **Update `.env` with refresh token**:
102+
103+
Copy the generated refresh token and add it to your `.env` file.
104+
105+
#### b. Temporary Token Flow (for development/testing only)
106+
107+
This method is quick and easy for **temporary testing**, but the token will **expire after 4 hours** and requires
108+
manual renewal. **Do not use this in production.**
109+
110+
###### Setup Steps:
111+
112+
1. **Generate a temporary access token:**
113+
114+
- Go to your [Dropbox App Console](https://www.dropbox.com/developers/apps).
115+
- Navigate to your app's settings.
116+
- Under "OAuth 2", find the "Generated access token" section and click "Generate". Copy this token.
117+
118+
2. **Add temporary token to `.env`:**
119+
120+
```dotenv
121+
DROPBOX_AUTH_MODE=temp_token
122+
DROPBOX_TEMP_TOKEN=
61123
```
62-
This is especially useful for custom automation (e.g., deployment scripts or remote backup workflows).
124+
125+
**Important:** If using this mode, the `DROPBOX_APP_KEY`, `DROPBOX_APP_SECRET`,
126+
and `DROPBOX_REFRESH_TOKEN` variables are ignored by the plugin.
127+
128+
### 3. Define the dropbox disk
129+
130+
Once you have configured your chosen authentication method in `.env`,
131+
define a new filesystem disk in your `config/filesystems.php` file:
132+
133+
```php
134+
'dropbox' => [
135+
'driver' => 'dropbox',
136+
],
137+
```
138+
139+
### 4. Usage with Storage Facade
140+
141+
You can now interact with Dropbox programmatically using the Storage facade in Laravel:
142+
143+
```php
144+
Storage::disk('dropbox')->put('backups/site.zip', $contents);
145+
```
146+
147+
This is especially useful for custom automation (e.g., deployment scripts or remote backup workflows).
63148

64149
## Limitations
150+
65151
- **Not compatible with Winter CMS native `media` or `uploads` disks.**
66152
- **Not suitable for asset serving or file uploading through the CMS backend UI.**
67153

68154
Use Dropbox through this plugin **only for custom filesystem operations** that are manually invoked or triggered
69155
via automation (e.g., within the [`NumenCode.SyncOps`](https://github.com/numencode/wn-syncops-plugin) plugin or similar).
70156

71157
## Example Use Case
158+
72159
This plugin was created to support [`NumenCode.SyncOps`](https://github.com/numencode/wn-syncops-plugin),
73160
a Winter CMS plugin for managing deployments, backups, and environment synchronization. Dropbox serves as a
74161
remote storage destination for sync packages or archive backups.

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
],
2020
"require": {
2121
"php": "^8.0.2",
22-
"winter/storm": "^1.2.7",
22+
"winter/storm": "^1.2",
2323
"spatie/flysystem-dropbox": "^3.0",
2424
"composer/installers": "~1.0"
2525
},

config/config.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
return [
4+
/*
5+
* The authentication mode for Dropbox.
6+
* Can be 'refresh_token' (recommended for production) or 'temp_token' (for quick testing).
7+
*/
8+
'auth_mode' => env('DROPBOX_AUTH_MODE', 'refresh_token'),
9+
10+
/*
11+
* Your Dropbox App Key
12+
*/
13+
'app_key' => env('DROPBOX_APP_KEY'),
14+
15+
/*
16+
* Your Dropbox App Secret
17+
*/
18+
'app_secret' => env('DROPBOX_APP_SECRET'),
19+
20+
/*
21+
* The Dropbox refresh token obtained via the `dropbox:setup` command.
22+
*/
23+
'refresh_token' => env('DROPBOX_REFRESH_TOKEN'),
24+
25+
/*
26+
* A temporary Dropbox access token for testing purposes.
27+
* Only used if 'auth_mode' is set to 'temp_token'.
28+
*/
29+
'temp_token' => env('DROPBOX_TEMP_TOKEN'),
30+
];

console/DropboxSetupCommand.php

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
namespace NumenCode\DropboxAdapter\Console;
4+
5+
use Winter\Storm\Console\Command;
6+
use Illuminate\Support\Facades\Http;
7+
8+
class DropboxSetupCommand extends Command
9+
{
10+
protected $signature = 'dropboxadapter:setup';
11+
12+
protected $description = 'Performs the initial setup to get a Dropbox refresh token.';
13+
14+
public function handle()
15+
{
16+
$this->info("--- Dropbox API Initial Setup ---\n");
17+
$this->line("This command will guide you through getting your permanent refresh token.");
18+
19+
// --- Step 1: Get App Key and construct the authorization URL ---
20+
$appKey = $this->ask('First, enter your Dropbox App Key');
21+
22+
if (!$appKey) {
23+
$this->error('App Key is required. Aborting.');
24+
return;
25+
}
26+
27+
$authUrl = 'https://www.dropbox.com/oauth2/authorize?client_id=' . $appKey . '&response_type=code&token_access_type=offline';
28+
29+
$this->line("\nGreat. Now, open the following URL in your browser, authorize the app, and then copy the 'code' you receive after being redirected:");
30+
$this->comment($authUrl); // Display the URL in a distinct color
31+
32+
// --- Step 2: Get the temporary code and the App Secret from the user ---
33+
$authCode = $this->ask("\nPaste the 'authorization code' here");
34+
$appSecret = $this->secret('Enter your Dropbox App Secret (input will be hidden)');
35+
36+
if (!$authCode || !$appSecret) {
37+
$this->error('Authorization Code and App Secret are required. Aborting.');
38+
return;
39+
}
40+
41+
$this->line("\nAttempting to exchange the authorization code for a refresh token...");
42+
43+
// --- Step 3: Make the API call to Dropbox using Laravel's HTTP Client ---
44+
$response = Http::asForm()->post('https://api.dropbox.com/oauth2/token', [
45+
'grant_type' => 'authorization_code',
46+
'code' => $authCode,
47+
'client_id' => $appKey,
48+
'client_secret' => $appSecret,
49+
]);
50+
51+
// --- Step 4: Process the response ---
52+
if ($response->failed()) {
53+
$this->error("\n[API Error] Failed to get the refresh token.");
54+
$this->line("Dropbox said: " . $response->body());
55+
$this->line("\nPlease double-check your credentials and try again.");
56+
return;
57+
}
58+
59+
$refreshToken = $response->json('refresh_token');
60+
61+
$this->info("\n✔ Success! Your refresh token has been generated.");
62+
$this->line("\nCopy the token below and add it to your .env file as DROPBOX_REFRESH_TOKEN:");
63+
64+
// Display the token in a prominent block
65+
$this->line('+----------------------------------------------------------------+');
66+
$this->line('| ');
67+
$this->line('| ' . $refreshToken);
68+
$this->line('| ');
69+
$this->line('+----------------------------------------------------------------+');
70+
71+
$this->line("\nAlso, make sure your .env file contains your App Key and Secret:\n");
72+
$this->comment("DROPBOX_APP_KEY=" . $appKey);
73+
$this->comment("DROPBOX_APP_SECRET=" . $appSecret);
74+
$this->comment("DROPBOX_REFRESH_TOKEN=" . $refreshToken);
75+
}
76+
}

providers/DropboxServiceProvider.php

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<?php namespace NumenCode\DropboxAdapter\Providers;
22

33
use League\Flysystem\Filesystem;
4+
use Illuminate\Support\Facades\Http;
5+
use Illuminate\Support\Facades\Cache;
6+
use Illuminate\Support\Facades\Config;
47
use Illuminate\Support\Facades\Storage;
58
use Illuminate\Support\ServiceProvider;
69
use Spatie\Dropbox\Client as DropboxClient;
@@ -12,8 +15,48 @@ class DropboxServiceProvider extends ServiceProvider
1215
public function boot()
1316
{
1417
Storage::extend('dropbox', function ($app, $config) {
18+
// Read from plugin config, which reads from .env by default
19+
$authMode = Config::get('numencode.dropboxadapter::auth_mode', 'refresh_token');
20+
21+
$accessToken = null;
22+
23+
if ($authMode === 'temp_token') {
24+
$accessToken = Config::get('numencode.dropboxadapter::temp_token');
25+
26+
if (!$accessToken) {
27+
throw new \InvalidArgumentException('DROPBOX_TEMP_TOKEN is required when DROPBOX_AUTH_MODE is set to \'temp_token\'.');
28+
}
29+
} else {
30+
if (!$accessToken = Cache::get('dropbox_access_token')) {
31+
$appKey = Config::get('numencode.dropboxadapter::app_key');
32+
$appSecret = Config::get('numencode.dropboxadapter::app_secret');
33+
$refreshToken = Config::get('numencode.dropboxadapter::refresh_token');
34+
35+
if (empty($refreshToken) || empty($appKey) || empty($appSecret)) {
36+
throw new \InvalidArgumentException('Missing Dropbox configuration for refresh token flow (DROPBOX_REFRESH_TOKEN, DROPBOX_APP_KEY, or DROPBOX_APP_SECRET).');
37+
}
38+
39+
$response = Http::asForm()->post('https://api.dropboxapi.com/oauth2/token', [
40+
'grant_type' => 'refresh_token',
41+
'refresh_token' => $refreshToken,
42+
'client_id' => $appKey,
43+
'client_secret' => $appSecret,
44+
]);
45+
46+
$response->throw();
47+
$data = $response->json();
48+
$accessToken = $data['access_token'];
49+
50+
Cache::put('dropbox_access_token', $accessToken, 12600);
51+
}
52+
}
53+
54+
if (!$accessToken) {
55+
throw new \RuntimeException('Failed to obtain a Dropbox access token.');
56+
}
57+
1558
$adapter = new DropboxAdapter(
16-
new DropboxClient($config['authorization_token'])
59+
new DropboxClient($accessToken)
1760
);
1861

1962
return new FilesystemAdapter(

0 commit comments

Comments
 (0)