├── LICENCE ├── README.md ├── appsscript.json ├── clientSide.html ├── images └── fig1.png └── serverSide.gs /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | Copyright (c) 2020 Kanshi TANAIKE 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 5 | 6 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 9 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Safe-Uploading for Google Drive by HTML in External Server using Google Apps Script 4 | 5 | 6 | 7 | # Overview 8 | 9 | **This is a report for safe-uploading files to Google Drive by HTML put in the external server using Google Apps Script.** 10 | 11 | 12 | 13 | # Description 14 | 15 | When you want to make the user upload a file to your own Google Drive using the HTML put in the external server of Google side, when the file size is smaller than 50 MB, this can be achieved without using the access token. [Ref](https://tanaikech.github.io/2020/08/13/uploading-file-to-google-drive-from-external-html-without-authorization/) (When the HTML is put in the internal server of Google side, you can also use [`google.script.run`](https://tanaikech.github.io/2020/02/18/uploading-file-to-google-drive-using-html-and-google-apps-script/).) But, when the file size is over 50 MB, it is required to upload the file with the resumable upload. In this case, the access token is required to be used. In this case that the user uploads to your own Google Drive, when the access token is used in the upload, it is considered that this is the weak point of the security. In this report, I would like to propose the method for safe-uploading files to Google Drive by HTML put in the external server using Google Apps Script. Please think of this as one of several methods. 16 | 17 |  18 | 19 | # Flow 20 | 21 | The flow of this method is as follows. 22 | 23 | 1. When an user access to the Web Apps and select a file for uploading from the user's local PC, the script of Web Apps is run. 24 | 25 | 2. Retrieve the access token and folder ID of destination folder (`temp` folder) to the client side. 26 | 27 | 3. Using the access token and folder ID, the file is uploaded with the resumable upload. 28 | 29 | - The script and javascript library for the resumable upload uses the repository of [ResumableUploadForGoogleDrive_js](https://github.com/tanaikech/ResumableUploadForGoogleDrive_js). 30 | 31 | 4. When the file upload is finished, at the Web Apps side, the uploaded file is copied to the specific folder (`dest` folder) and the original file is removed. By this, the owner of the file is changed from the service account to your account. By this, the access token from the Web Apps cannot see the uploaded file. 32 | 33 | # Usage 34 | 35 | ## 1. Create service account. 36 | 37 | This method uses the service account. So please create the service account. You can see the method for creating the service account as follows. 38 | 39 | - [Creating and managing service accounts](https://cloud.google.com/iam/docs/creating-managing-service-accounts?hl=en) 40 | - [Create a service account](https://support.google.com/a/answer/7378726?hl=en) 41 | 42 | When you create the service account, please retrieve the credential file as the JSON file. The credential data is used in this method. 43 | 44 | ## 2. Create new folders. 45 | 46 | 1. Please create 2 new folders. For example, please set the folder names like `temp` and `dest`. One is used for the temporal folder. Another is used for the destination folder of the uploaded file. 47 | 48 | 2. Please share `temp` folder with the email address of the service account as the writer. By this, the service account can access to the folder. The folder `dest` is NOT required to be shared. 49 | 50 | ## 3. Create new project of Google Apps Script. 51 | 52 | This Web Apps is created by Google Apps Script. So please create new Google Apps Script project. 53 | 54 | If you want to directly create it, please access to [https://script.new/](https://script.new/). In this case, if you are not logged in Google, the log in screen is opened. So please log in to Google. By this, the script editor of Google Apps Script is opened. 55 | 56 | ## 4. Prepare Web Apps side. (server side) 57 | 58 | Please copy and paste the following script (Google Apps Script) to the script editor. This script is for the Web Apps. This Web Apps is used as an API. 59 | 60 | And, please set the variables of `folderId` and `dstFolderId` in `doGet`, and `private_key` and `client_email` in `getAccessTokenFromServiceAccount`. 61 | 62 | And also, [please enable Drive API at Advanced Google services](https://developers.google.com/apps-script/guides/services/advanced#enable_advanced_services). 63 | 64 | ```javascript 65 | function doGet(e) { 66 | const tempFolderId = "###"; // Please set the folder ID of folder shared with the service account. 67 | const destFolderId = "###"; // Please set the destination folder for the uploaded file. 68 | 69 | const accessKey = "sampleKey"; // This is used as the API key for using of this script. 70 | 71 | const key = e.parameter.key; 72 | if (key && key == accessKey) { 73 | const work = e.parameter.work; 74 | if (work && work == "start") { 75 | const scopes = ["https://www.googleapis.com/auth/drive.file"]; 76 | const accessToken = getAccessTokenFromServiceAccount(scopes); 77 | const resValue = Utilities.base64Encode( 78 | JSON.stringify({ accessToken: accessToken, folderId: tempFolderId }) 79 | ); 80 | return ContentService.createTextOutput(resValue).setMimeType( 81 | ContentService.MimeType.TEXT 82 | ); 83 | } else if (work && work == "end") { 84 | const fileId = e.parameter.fileId; 85 | const params1 = { 86 | method: "post", 87 | contentType: "application/json", 88 | payload: JSON.stringify({ parents: [destFolderId] }), 89 | headers: { authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, 90 | }; 91 | UrlFetchApp.fetch( 92 | `https://www.googleapis.com/drive/v3/files/${fileId}/copy`, 93 | params1 94 | ); 95 | const scopes = ["https://www.googleapis.com/auth/drive"]; 96 | const accessToken = getAccessTokenFromServiceAccount(scopes); 97 | const params2 = { 98 | method: "delete", 99 | headers: { authorization: `Bearer ${accessToken}` }, 100 | }; 101 | UrlFetchApp.fetch( 102 | `https://www.googleapis.com/drive/v3/files/${fileId}`, 103 | params2 104 | ); 105 | return ContentService.createTextOutput("Done.").setMimeType( 106 | ContentService.MimeType.TEXT 107 | ); 108 | } 109 | } 110 | return ContentService.createTextOutput("Error.").setMimeType( 111 | ContentService.MimeType.TEXT 112 | ); 113 | 114 | // DriveApp.createFile() // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive". This scope is used for copying file. 115 | } 116 | 117 | // This function is used for retrieving the access token from the service account. 118 | // This script is from https://tanaikech.github.io/2018/12/07/retrieving-access-token-for-service-account-using-google-apps-script/ 119 | function getAccessTokenFromServiceAccount(scopes) { 120 | const private_key = 121 | "-----BEGIN PRIVATE KEY-----\n###your provate key###-----END PRIVATE KEY-----\n"; // private_key of JSON file retrieved by creating Service Account 122 | const client_email = "###"; // client_email of JSON file retrieved by creating Service Account 123 | 124 | const url = "https://www.googleapis.com/oauth2/v4/token"; 125 | const header = { alg: "RS256", typ: "JWT" }; 126 | const now = Math.floor(Date.now() / 1000); 127 | const claim = { 128 | iss: client_email, 129 | scope: scopes.join(" "), 130 | aud: url, 131 | exp: (now + 3600).toString(), 132 | iat: now.toString(), 133 | }; 134 | const signature = 135 | Utilities.base64Encode(JSON.stringify(header)) + 136 | "." + 137 | Utilities.base64Encode(JSON.stringify(claim)); 138 | const jwt = 139 | signature + 140 | "." + 141 | Utilities.base64Encode( 142 | Utilities.computeRsaSha256Signature(signature, private_key) 143 | ); 144 | const params = { 145 | method: "post", 146 | payload: { 147 | assertion: jwt, 148 | grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", 149 | }, 150 | }; 151 | const data = UrlFetchApp.fetch(url, params).getContentText(); 152 | const obj = JSON.parse(data); 153 | return obj.access_token; 154 | } 155 | ``` 156 | 157 | Above script can be also seen by `serverSide.gs`. 158 | 159 | ## 5. Deploy Web Apps. 160 | 161 | Here, the Web Apps is deployed. 162 | 163 | 1. On the script editor, Open a dialog box by "Publish" -> "Deploy as web app". 164 | 165 | - You can see this for "Legacy editor" and "New editor" at [official document](https://developers.google.com/apps-script/guides/web#deploy_a_script_as_a_web_app). 166 | 167 | 2. Select **"Me"** for **"Execute the app as:"** on "Legacy editor" and **"Execute as:** on "New editor". 168 | 169 | - By this, the script is run as the owner. 170 | 171 | 3. Select **"Anyone, even anonymous"** for **"Who has access to the app:"** on "Legacy editor" and **"Anyone"** on "New editor". 172 | 173 | - At the client and server side, the access key is used for executing the script. 174 | 175 | 4. Click "Deploy" button as new "Project version". 176 | 177 | 5. Automatically open a dialog box of "Authorization required". 178 | 179 | 1. Click "Review Permissions". 180 | 2. Select own account. 181 | 3. Click "Advanced" at "This app isn't verified". 182 | 4. Click "Go to ### project name ###(unsafe)" 183 | 5. Click "Allow" button. 184 | 185 | 6. Click "OK". 186 | 187 | 7. Copy the URL of Web Apps. It's like `https://script.google.com/macros/s/###/exec`. 188 | 189 | - **When you modified the Google Apps Script, please redeploy as new version. By this, the modified script is reflected to Web Apps. Please be careful this.** 190 | 191 | You can see the detail information of Web Apps at [here](https://developers.google.com/apps-script/guides/web) and [here](https://github.com/tanaikech/taking-advantage-of-Web-Apps-with-google-apps-script). 192 | 193 | ## 6. Testing. (client side) 194 | 195 | Please copy and paste the following script (HTML and Javascript) to a file. This file can be put outside of GAS project. This script is for uploading files to Google Drive. 196 | 197 | Please set the variable of `webAppsUrl` to the URL of your deployed Web Apps. 198 | 199 | ```html 200 | 201 | 202 |
203 | 206 | 207 | 208 | 209 | 261 | ``` 262 | 263 | Above script can be also seen by `clientSide.html`. 264 | 265 | - The script for uploading file with the resumable upload is from [ResumableUploadForGoogleDrive_js](https://github.com/tanaikech/ResumableUploadForGoogleDrive_js). Using this script, the file is uploaded to Google Drive. 266 | 267 | # Note 268 | 269 | - In this method, the uploaded file is saved by changing the owner of the uploaded file using the service account. So in order to change the owner of the uploaded file, the uploaded file is copied. When the owner of the file can be directly changed, the process cost will be able to be reduced more. But in the current stage, when the owner of the files, which are not Google Docs files, of service account is changed, an error of `Bad Request. User message: \"You can't change the owner of this item.\"` occurs. So I changed the owner of file by copying. 270 | 271 | - **In order to save the uploaded file, the owner of uploaded file is changed by copying the uploaded file. So, when the file size is large, please be careful the remaining storage capacity.** 272 | 273 | - As other method, I think that the temp folder in above method can be created in the Drive of service account. In that case, it is required to share the temp folder with your Google account. Please be careful this. 274 | 275 | # References 276 | 277 | - [Uploading File to Google Drive from External HTML without Authorization](https://tanaikech.github.io/2020/08/13/uploading-file-to-google-drive-from-external-html-without-authorization/) 278 | - [Uploading File to Google Drive using HTML and Google Apps Script](https://tanaikech.github.io/2020/02/18/uploading-file-to-google-drive-using-html-and-google-apps-script/) 279 | - [ResumableUploadForGoogleDrive_js](https://github.com/tanaikech/ResumableUploadForGoogleDrive_js) 280 | - [Taking advantage of Web Apps with Google Apps Script](https://github.com/tanaikech/taking-advantage-of-Web-Apps-with-google-apps-script) 281 | 282 | --- 283 | 284 | 285 | 286 | # Licence 287 | 288 | [MIT](LICENCE) 289 | 290 | 291 | 292 | # Author 293 | 294 | [Tanaike](https://tanaikech.github.io/about/) 295 | 296 | If you have any questions and commissions for me, feel free to tell me. 297 | 298 | 299 | 300 | # Update History 301 | 302 | - v1.0.0 (December 29, 2020) 303 | 304 | 1. Initial release. 305 | 306 | [TOP](#top) 307 | -------------------------------------------------------------------------------- /appsscript.json: -------------------------------------------------------------------------------- 1 | { 2 | "timeZone": "Asia/Tokyo", 3 | "dependencies": {}, 4 | "exceptionLogging": "STACKDRIVER", 5 | "runtimeVersion": "V8" 6 | } 7 | -------------------------------------------------------------------------------- /clientSide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 8 | 9 | 10 | 62 | -------------------------------------------------------------------------------- /images/fig1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tanaikech/Safe-Uploading-for-Google-Drive-by-HTML-in-External-Server-using-Google-Apps-Script/46b07fbd319eee1adc8974b696cd95300317129b/images/fig1.png -------------------------------------------------------------------------------- /serverSide.gs: -------------------------------------------------------------------------------- 1 | function doGet(e) { 2 | const tempFolderId = "###"; // Please set the folder ID of folder shared with the service account. 3 | const destFolderId = "###"; // Please set the destination folder for the uploaded file. 4 | 5 | const accessKey = "sampleKey"; // This is used as the API key for using of this script. 6 | 7 | const key = e.parameter.key; 8 | if (key && key == accessKey) { 9 | const work = e.parameter.work; 10 | if (work && work == "start") { 11 | const scopes = ["https://www.googleapis.com/auth/drive.file"]; 12 | const accessToken = getAccessTokenFromServiceAccount(scopes); 13 | const resValue = Utilities.base64Encode( 14 | JSON.stringify({ accessToken: accessToken, folderId: tempFolderId }) 15 | ); 16 | return ContentService.createTextOutput(resValue).setMimeType( 17 | ContentService.MimeType.TEXT 18 | ); 19 | } else if (work && work == "end") { 20 | const fileId = e.parameter.fileId; 21 | const params1 = { 22 | method: "post", 23 | contentType: "application/json", 24 | payload: JSON.stringify({ parents: [destFolderId] }), 25 | headers: { authorization: `Bearer ${ScriptApp.getOAuthToken()}` }, 26 | }; 27 | UrlFetchApp.fetch( 28 | `https://www.googleapis.com/drive/v3/files/${fileId}/copy`, 29 | params1 30 | ); 31 | const scopes = ["https://www.googleapis.com/auth/drive"]; 32 | const accessToken = getAccessTokenFromServiceAccount(scopes); 33 | const params2 = { 34 | method: "delete", 35 | headers: { authorization: `Bearer ${accessToken}` }, 36 | }; 37 | UrlFetchApp.fetch( 38 | `https://www.googleapis.com/drive/v3/files/${fileId}`, 39 | params2 40 | ); 41 | return ContentService.createTextOutput("Done.").setMimeType( 42 | ContentService.MimeType.TEXT 43 | ); 44 | } 45 | } 46 | return ContentService.createTextOutput("Error.").setMimeType( 47 | ContentService.MimeType.TEXT 48 | ); 49 | 50 | // DriveApp.createFile() // This is used for automatically detecting the scope of "https://www.googleapis.com/auth/drive". This scope is used for copying file. 51 | } 52 | 53 | // This function is used for retrieving the access token from the service account. 54 | // This script is from https://tanaikech.github.io/2018/12/07/retrieving-access-token-for-service-account-using-google-apps-script/ 55 | function getAccessTokenFromServiceAccount(scopes) { 56 | const private_key = 57 | "-----BEGIN PRIVATE KEY-----\n###your provate key###-----END PRIVATE KEY-----\n"; // private_key of JSON file retrieved by creating Service Account 58 | const client_email = "###"; // client_email of JSON file retrieved by creating Service Account 59 | 60 | const url = "https://www.googleapis.com/oauth2/v4/token"; 61 | const header = { alg: "RS256", typ: "JWT" }; 62 | const now = Math.floor(Date.now() / 1000); 63 | const claim = { 64 | iss: client_email, 65 | scope: scopes.join(" "), 66 | aud: url, 67 | exp: (now + 3600).toString(), 68 | iat: now.toString(), 69 | }; 70 | const signature = 71 | Utilities.base64Encode(JSON.stringify(header)) + 72 | "." + 73 | Utilities.base64Encode(JSON.stringify(claim)); 74 | const jwt = 75 | signature + 76 | "." + 77 | Utilities.base64Encode( 78 | Utilities.computeRsaSha256Signature(signature, private_key) 79 | ); 80 | const params = { 81 | method: "post", 82 | payload: { 83 | assertion: jwt, 84 | grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer", 85 | }, 86 | }; 87 | const data = UrlFetchApp.fetch(url, params).getContentText(); 88 | const obj = JSON.parse(data); 89 | return obj.access_token; 90 | } 91 | --------------------------------------------------------------------------------