From 672fecb1d33a48b2968d574570ae96f7e886d962 Mon Sep 17 00:00:00 2001 From: Marcel Date: Sun, 24 Dec 2023 00:10:32 +0200 Subject: [PATCH] Support dropbox refresh token (#3) * Add dynamic access token fetching using a refresh token * Fix indentation * Extract access token retrieval into separate method * Simplify blank check for multiple variables --------- Co-authored-by: Can --- .github/workflows/build-run.yml | 5 +- .github/workflows/docker-run.yml | 3 ++ README.md | 3 ++ documentation/setup.md | 51 ++++++++++++++++--- .../greydev/notionbackup/NotionBackup.java | 47 +++++++++++++++-- 5 files changed, 97 insertions(+), 12 deletions(-) diff --git a/.github/workflows/build-run.yml b/.github/workflows/build-run.yml index 14b024e..a2b7843 100644 --- a/.github/workflows/build-run.yml +++ b/.github/workflows/build-run.yml @@ -35,8 +35,11 @@ jobs: GOOGLE_DRIVE_SERVICE_ACCOUNT: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT }} GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_JSON: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_JSON }} GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_FILE_PATH: ${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_FILE_PATH }} - + DROPBOX_ACCESS_TOKEN: ${{ secrets.DROPBOX_ACCESS_TOKEN }} + DROPBOX_APP_KEY: ${{ secrets.DROPBOX_APP_KEY }} + DROPBOX_APP_SECRET: ${{ secrets.DROPBOX_APP_SECRET }} + DROPBOX_REFRESH_TOKEN: ${{ secrets.DROPBOX_REFRESH_TOKEN }} NEXTCLOUD_EMAIL: ${{ secrets.NEXTCLOUD_EMAIL }} NEXTCLOUD_PASSWORD: ${{ secrets.NEXTCLOUD_PASSWORD }} diff --git a/.github/workflows/docker-run.yml b/.github/workflows/docker-run.yml index bad148f..3b34ee8 100644 --- a/.github/workflows/docker-run.yml +++ b/.github/workflows/docker-run.yml @@ -27,6 +27,9 @@ jobs: GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_FILE_PATH=${{ secrets.GOOGLE_DRIVE_SERVICE_ACCOUNT_SECRET_FILE_PATH }} DROPBOX_ACCESS_TOKEN=${{ secrets.DROPBOX_ACCESS_TOKEN }} + DROPBOX_APP_KEY=${{ secrets.DROPBOX_APP_KEY }} + DROPBOX_APP_SECRET=${{ secrets.DROPBOX_APP_SECRET }} + DROPBOX_REFRESH_TOKEN=${{ secrets.DROPBOX_REFRESH_TOKEN }} NEXTCLOUD_EMAIL=${{ secrets.NEXTCLOUD_EMAIL }} NEXTCLOUD_PASSWORD=${{ secrets.NEXTCLOUD_PASSWORD }} diff --git a/README.md b/README.md index 790dfee..19dd4a4 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,9 @@ Create a `.env` file with the following properties ([How do I find all these val # Dropbox (Optional) DROPBOX_ACCESS_TOKEN= + DROPBOX_APP_KEY= + DROPBOX_APP_SECRET= + DROPBOX_REFRESH_TOKEN= # Nextcloud (Optional) NEXTCLOUD_EMAIL= diff --git a/documentation/setup.md b/documentation/setup.md index f727575..7e7bb0e 100644 --- a/documentation/setup.md +++ b/documentation/setup.md @@ -16,12 +16,51 @@ ### Dropbox 1. Create a new app on developer console (https://www.dropbox.com/developers/apps/create) -2. Select "Scoped access" > "App folder – Access to a single folder created specifically for your app." and give - your app a name -3. Go to permissions tab > enable `files.content.write` & `files.content.read` and click "Submit" to save your changes. - Make sure you saved these changes **before** you generate your access token. -4. Go to Settings > OAuth 2 > Generate access token > "generate" and copy the generated token. Paste it in your - `.env` file as the value for `DROPBOX_ACCESS_TOKEN` +2. Go to the Permissions tab > enable `files.content.write` & `files.content.read` and click "Submit" to save your changes. +Make sure you saved these changes **before** you generate your access token. +3. Go to the Settings tab > OAuth 2 > Generate access token > Generate. Note that these tokens are short-lived and expire after a few hours. + Due to security reason long-lived tokens have been deprecated. + +For an automated setup, for example with [GitHub Action](../README.md#fork-github-actions), short-lived access tokens will not work because they will expire. +An alternative solution would be to use a refresh token that does not expire which you need to retrieve manually once. +The refresh token will then be used to fetch an access token on-the-fly everytime the application runs. +Do the following steps to set up the refresh token flow. + +1. Go to the Settings tab and store the App Key in the `DROPBOX_APP_KEY` environment variable +2. Go to the Settings tab and store the App Secret in the `DROPBOX_APP_SECRET` environment variable +3. Open a browser and enter the following URL and replace the `` placeholder + + https://www.dropbox.com/oauth2/authorize?client_id=&token_access_type=offline&response_type=code + +4. Click on continue and allow the app to access your files +5. Copy the authorization code (referred to as `AUTH_CODE` in the following steps) shown in the following screen +6. Send the following HTTP POST request with the actual values replacing the placeholders + +``` +curl --request POST \ + --url https://api.dropboxapi.com/oauth2/token \ + --header 'Authorization: Basic :>' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data code= \ + --data grant_type=authorization_code +``` + +7. Extract the `refresh_token` from the response and store it in the `DROPBOX_REFRESH_TOKEN` environment variable. + **DO NOT SHARE THIS TOKEN WITH ANYONE!** + +8. To ensure it works try sending the following HTTP POST request with the actual values replacing the placeholders + +``` +curl --request POST \ + --url https://api.dropbox.com/oauth2/token \ + --header 'Authorization: Basic :>' \ + --header 'Content-Type: application/x-www-form-urlencoded' \ + --data grant_type=refresh_token \ + --data refresh_token= +``` + +9. Leave the `DROPBOX_ACCESS_TOKEN` environment variable empty. + Otherwise, the refresh token flow will be skipped and no new access token will be fetched. ### Nextcloud diff --git a/src/main/java/com/greydev/notionbackup/NotionBackup.java b/src/main/java/com/greydev/notionbackup/NotionBackup.java index b55353e..0ecd371 100644 --- a/src/main/java/com/greydev/notionbackup/NotionBackup.java +++ b/src/main/java/com/greydev/notionbackup/NotionBackup.java @@ -14,6 +14,10 @@ import org.apache.commons.lang3.StringUtils; import com.dropbox.core.v2.DbxClientV2; +import com.dropbox.core.DbxException; +import com.dropbox.core.DbxRequestConfig; +import com.dropbox.core.oauth.DbxCredential; +import com.dropbox.core.oauth.DbxRefreshResult; import com.google.api.services.drive.Drive; import com.greydev.notionbackup.cloudstorage.dropbox.DropboxClient; import com.greydev.notionbackup.cloudstorage.dropbox.DropboxServiceFactory; @@ -29,6 +33,9 @@ public class NotionBackup { public static final String KEY_DROPBOX_ACCESS_TOKEN = "DROPBOX_ACCESS_TOKEN"; + public static final String KEY_DROPBOX_APP_KEY = "DROPBOX_APP_KEY"; + public static final String KEY_DROPBOX_APP_SECRET = "DROPBOX_APP_SECRET"; + public static final String KEY_DROPBOX_REFRESH_TOKEN = "DROPBOX_REFRESH_TOKEN"; public static final String KEY_NEXTCLOUD_EMAIL = "NEXTCLOUD_EMAIL"; public static final String KEY_NEXTCLOUD_PASSWORD = "NEXTCLOUD_PASSWORD"; @@ -90,13 +97,13 @@ public static void startGoogleDriveBackup(File fileToUpload) { public static void startDropboxBackup(File fileToUpload) { - String dropboxAccessToken = dotenv.get(KEY_DROPBOX_ACCESS_TOKEN); - - if (StringUtils.isBlank(dropboxAccessToken)) { - log.info("Skipping Dropbox upload. {} is blank.", KEY_DROPBOX_ACCESS_TOKEN); + Optional dropboxAccessToken = getDropboxAccessToken(); + if (dropboxAccessToken.isEmpty()) { + log.info("No Dropbox access token available. Skipping Dropbox upload."); return; } - Optional dropboxServiceOptional = DropboxServiceFactory.create(dropboxAccessToken); + + Optional dropboxServiceOptional = DropboxServiceFactory.create(dropboxAccessToken.get()); if (dropboxServiceOptional.isEmpty()) { log.warn("Could not create Dropbox service. Skipping Dropbox upload"); return; @@ -105,6 +112,36 @@ public static void startDropboxBackup(File fileToUpload) { dropboxClient.upload(fileToUpload); } + private static Optional getDropboxAccessToken() { + String dropboxAccessToken = dotenv.get(KEY_DROPBOX_ACCESS_TOKEN); + + if (StringUtils.isBlank(dropboxAccessToken)) { + log.info("{} is blank. Trying to fetch an access token with the refresh token...", KEY_DROPBOX_ACCESS_TOKEN); + + String dropboxAppKey = dotenv.get(KEY_DROPBOX_APP_KEY); + String dropboxAppSecret = dotenv.get(KEY_DROPBOX_APP_SECRET); + String dropboxRefreshToken = dotenv.get(KEY_DROPBOX_REFRESH_TOKEN); + if (StringUtils.isAnyBlank(dropboxAppKey, dropboxAppSecret, dropboxRefreshToken)) { + log.info("Failed to fetch an access token. Either {}, {} or {} is blank.", KEY_DROPBOX_REFRESH_TOKEN, KEY_DROPBOX_APP_KEY, KEY_DROPBOX_APP_SECRET); + return Optional.empty(); + } + + DbxCredential dbxCredential = new DbxCredential("", 14400L, dropboxRefreshToken, dropboxAppKey, dropboxAppSecret); + DbxRefreshResult refreshResult; + try { + refreshResult = dbxCredential.refresh(new DbxRequestConfig("NotionBackup")); + } catch (DbxException e) { + log.info("Token refresh call to Dropbox API failed."); + return Optional.empty(); + } + + dropboxAccessToken = refreshResult.getAccessToken(); + log.info("Successfully fetched an access token."); + } + + return Optional.of(dropboxAccessToken); + } + public static void startNextcloudBackup(File fileToUpload) { String email = dotenv.get(KEY_NEXTCLOUD_EMAIL);