basic dropbox auth flow

This commit is contained in:
fyears 2021-11-28 16:28:52 +08:00
parent 00c840a758
commit 1effcdd7f7
4 changed files with 306 additions and 18 deletions

View File

@ -28,7 +28,14 @@ import { isPasswordOk, getSyncPlan, doActualSync } from "./sync";
import { S3Config, DEFAULT_S3_CONFIG } from "./s3"; import { S3Config, DEFAULT_S3_CONFIG } from "./s3";
import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav"; import { WebdavConfig, DEFAULT_WEBDAV_CONFIG, WebdavAuthType } from "./webdav";
import { DropboxConfig, DEFAULT_DROPBOX_CONFIG } from "./remoteForDropbox"; import {
DropboxConfig,
DEFAULT_DROPBOX_CONFIG,
getCodeVerifierAndChallenge,
getAuthUrl,
sendAuthReq,
setConfigBySuccessfullAuthInplace,
} from "./remoteForDropbox";
import { RemoteClient } from "./remote"; import { RemoteClient } from "./remote";
import { exportSyncPlansToFiles } from "./debugMode"; import { exportSyncPlansToFiles } from "./debugMode";
@ -185,7 +192,11 @@ export default class RemotelySavePlugin extends Plugin {
} }
async loadSettings() { async loadSettings() {
this.settings = Object.assign({}, DEFAULT_SETTINGS, await this.loadData()); this.settings = Object.assign(
{},
JSON.parse(JSON.stringify(DEFAULT_SETTINGS)) /* copy an object */,
await this.loadData()
);
} }
async saveSettings() { async saveSettings() {
@ -259,6 +270,108 @@ export class PasswordModal extends Modal {
} }
} }
export class DropboxAuthModal extends Modal {
readonly plugin: RemotelySavePlugin;
readonly authDiv: HTMLDivElement;
readonly revokeAuthDiv: HTMLDivElement;
readonly revokeAuthSetting: Setting;
constructor(
app: App,
plugin: RemotelySavePlugin,
authDiv: HTMLDivElement,
revokeAuthDiv: HTMLDivElement,
revokeAuthSetting: Setting
) {
super(app);
this.plugin = plugin;
this.authDiv = authDiv;
this.revokeAuthDiv = revokeAuthDiv;
this.revokeAuthSetting = revokeAuthSetting;
}
onOpen() {
let { contentEl } = this;
const k = getCodeVerifierAndChallenge();
const authUrl = getAuthUrl(
this.plugin.settings.dropbox.clientID,
k.challenge
);
contentEl.createEl("p", {
text: "Step 1: Visit the following address in a browser, and follow the steps on the web page to authorize.",
});
contentEl.createEl("p").createEl("a", {
href: authUrl,
text: authUrl,
});
contentEl.createEl("p", {
text: 'Step 2: In the end of the web flow, you obtain a long code. Paste it here then click "Submit".',
});
let authCode = "";
new Setting(contentEl)
.setName("Auth Code from web page")
.setDesc('You need to click "Confirm".')
.addText((text) =>
text
.setPlaceholder("")
.setValue("")
.onChange((val) => {
authCode = val.trim();
})
)
.addButton(async (button) => {
button.setButtonText("Confirm");
button.onClick(async () => {
new Notice("Trying to connect to Dropbox");
try {
const authRes = await sendAuthReq(
this.plugin.settings.dropbox.clientID,
k.verifier,
authCode
);
setConfigBySuccessfullAuthInplace(
this.plugin.settings.dropbox,
authRes
);
const client = new RemoteClient(
"dropbox",
undefined,
undefined,
this.plugin.settings.dropbox
);
const username = await client.getUser();
this.plugin.settings.dropbox.username = username;
await this.plugin.saveSettings();
new Notice(`Good! We've connected to Dropbox as user ${username}!`);
this.authDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
this.revokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
this.revokeAuthSetting.setDesc(
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
);
this.close();
} catch (err) {
console.log(err);
new Notice("Something goes wrong while connecting to Dropbox.");
}
});
});
}
onClose() {
let { contentEl } = this;
contentEl.empty();
}
}
class RemotelySaveSettingTab extends PluginSettingTab { class RemotelySaveSettingTab extends PluginSettingTab {
plugin: RemotelySavePlugin; plugin: RemotelySavePlugin;
@ -299,11 +412,11 @@ class RemotelySaveSettingTab extends PluginSettingTab {
}); });
}); });
// we need to create the div in advance of s3Div and webdavDiv // we need to create the div in advance of any other service divs
const serviceChooserDiv = generalDiv.createEl("div"); const serviceChooserDiv = generalDiv.createEl("div");
let clickChooserTimes = 0; let clickChooserTimes = 0;
serviceChooserDiv.onClickEvent((x) => { serviceChooserDiv.onClickEvent(async (x) => {
if (Platform.isIosApp) { if (Platform.isIosApp) {
// downgrade the experiment // downgrade the experiment
// because iOS doesn't support x.detail // because iOS doesn't support x.detail
@ -320,13 +433,13 @@ class RemotelySaveSettingTab extends PluginSettingTab {
); );
} else if (!this.plugin.settings.enableExperimentService) { } else if (!this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = true; this.plugin.settings.enableExperimentService = true;
this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice( new Notice(
"You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution." "You've enabled hidden unstable experimental webdav support. Reopen settings again and try webdav with caution."
); );
} else if (this.plugin.settings.enableExperimentService) { } else if (this.plugin.settings.enableExperimentService) {
this.plugin.settings.enableExperimentService = false; this.plugin.settings.enableExperimentService = false;
this.plugin.saveSettings(); await this.plugin.saveSettings();
new Notice( new Notice(
"You've disabled hidden unstable experimental webdav support. Reopen settings again." "You've disabled hidden unstable experimental webdav support. Reopen settings again."
); );
@ -474,21 +587,77 @@ class RemotelySaveSettingTab extends PluginSettingTab {
cls: "dropbox-disclaimer", cls: "dropbox-disclaimer",
}); });
dropboxDiv.createEl("p", { dropboxDiv.createEl("p", {
text: "We create a folder App/obsidian-remotely-save on your Dropbox. All files/folders sync would happen inside this folder.", text: "We will create a folder App/obsidian-remotely-save on your Dropbox. All files/folders sync would happen inside this folder.",
}); });
new Setting(dropboxDiv) const dropboxSelectAuthDiv = dropboxDiv.createDiv();
.setName("access token") const dropboxAuthDiv = dropboxSelectAuthDiv.createDiv({
.setDesc("access token") cls: "dropbox-auth-button-hide",
.addText((text) => });
text const dropboxRevokeAuthDiv = dropboxSelectAuthDiv.createDiv({
.setPlaceholder("") cls: "dropbox-revoke-auth-button-hide",
.setValue(this.plugin.settings.dropbox.accessToken) });
.onChange(async (value) => {
this.plugin.settings.dropbox.accessToken = value.trim(); const revokeAuthSetting = new Setting(dropboxRevokeAuthDiv)
.setName("Revoke Auth")
.setDesc(
`You've connected as user ${this.plugin.settings.dropbox.username}. If you want to disconnect, click this button`
)
.addButton(async (button) => {
button.setButtonText("Revoke Auth");
button.onClick(async () => {
try {
console.log("settings ");
console.log(this.plugin.settings.dropbox);
const client = new RemoteClient(
"dropbox",
undefined,
undefined,
this.plugin.settings.dropbox
);
await client.revokeAuth();
this.plugin.settings.dropbox = { ...DEFAULT_DROPBOX_CONFIG };
await this.plugin.saveSettings(); await this.plugin.saveSettings();
}) dropboxAuthDiv.toggleClass(
); "dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
dropboxRevokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
new Notice("Revoked!");
} catch (err) {
console.log(err);
new Notice("Something goes wrong while revoking");
}
});
});
new Setting(dropboxAuthDiv)
.setName("Auth")
.setDesc("Auth")
.addButton(async (button) => {
button.setButtonText("Auth");
button.onClick(async () => {
new DropboxAuthModal(
this.app,
this.plugin,
dropboxAuthDiv,
dropboxRevokeAuthDiv,
revokeAuthSetting
).open();
});
});
dropboxAuthDiv.toggleClass(
"dropbox-auth-button-hide",
this.plugin.settings.dropbox.username !== ""
);
dropboxRevokeAuthDiv.toggleClass(
"dropbox-revoke-auth-button-hide",
this.plugin.settings.dropbox.username === ""
);
new Setting(dropboxDiv) new Setting(dropboxDiv)
.setName("check connectivity") .setName("check connectivity")

View File

@ -188,4 +188,20 @@ export class RemoteClient {
throw Error(`not supported service type ${this.serviceType}`); throw Error(`not supported service type ${this.serviceType}`);
} }
}; };
getUser = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.getUserDisplayName(this.dropboxClient);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
};
revokeAuth = async () => {
if (this.serviceType === "dropbox") {
return await dropbox.revokeAuth(this.dropboxClient);
} else {
throw Error(`not supported service type ${this.serviceType}`);
}
};
} }

View File

@ -1,5 +1,7 @@
import * as path from "path"; import * as path from "path";
import { FileStats, Vault } from "obsidian"; import { FileStats, Vault } from "obsidian";
import { Buffer } from "buffer";
import * as crypto from "crypto";
import { Dropbox, DropboxResponse, files } from "dropbox"; import { Dropbox, DropboxResponse, files } from "dropbox";
export { Dropbox } from "dropbox"; export { Dropbox } from "dropbox";
@ -17,10 +19,22 @@ import { strict as assert } from "assert";
export interface DropboxConfig { export interface DropboxConfig {
accessToken: string; accessToken: string;
clientID: string;
refreshToken: string;
accessTokenExpiresInSeconds: number;
accessTokenExpiresAtTime: number;
accountID: string;
username: string;
} }
export const DEFAULT_DROPBOX_CONFIG = { export const DEFAULT_DROPBOX_CONFIG = {
accessToken: "", accessToken: "",
clientID: "",
refreshToken: "",
accessTokenExpiresInSeconds: 0,
accessTokenExpiresAtTime: 0,
accountID: "",
username: "",
}; };
export const getDropboxPath = (fileOrFolderPath: string) => { export const getDropboxPath = (fileOrFolderPath: string) => {
@ -370,3 +384,84 @@ export const checkConnectivity = async (dbx: Dropbox) => {
return false; return false;
} }
}; };
export const getUserDisplayName = async (dbx: Dropbox) => {
const acct = await dbx.usersGetCurrentAccount();
return acct.result.name.display_name;
};
/**
* Dropbox authorization using PKCE
* see https://dropbox.tech/developers/pkce--what-and-why-
*
*/
const specialBase64Encode = (str: Buffer) => {
return str
.toString("base64")
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=/g, "");
};
const sha256 = (buffer: string) => {
return crypto.createHash("sha256").update(buffer).digest();
};
export const getCodeVerifierAndChallenge = () => {
const codeVerifier = specialBase64Encode(crypto.randomBytes(32));
// console.log(`Client generated code_verifier: ${codeVerifier}`);
const codeChallenge = specialBase64Encode(sha256(codeVerifier));
// console.log(`Client generated code_challenge: ${codeChallenge}`);
return {
verifier: codeVerifier,
challenge: codeChallenge,
};
};
export const getAuthUrl = (appKey: string, challenge: string) => {
return `https://www.dropbox.com/oauth2/authorize?client_id=${appKey}&response_type=code&code_challenge=${challenge}&code_challenge_method=S256&token_access_type=offline`;
};
export interface DropboxSuccessAuthRes {
access_token: string;
token_type: string;
expires_in: number;
refresh_token: string;
scope: string;
uid: string;
account_id: string;
}
export const sendAuthReq = async (
appKey: string,
verifier: string,
authCode: string
) => {
const resp1 = await fetch("https://api.dropboxapi.com/oauth2/token", {
method: "POST",
body: new URLSearchParams({
code: authCode,
grant_type: "authorization_code",
code_verifier: verifier,
client_id: appKey,
}),
});
const resp2 = (await resp1.json()) as DropboxSuccessAuthRes;
return resp2;
};
export const setConfigBySuccessfullAuthInplace = (
config: DropboxConfig,
authRes: DropboxSuccessAuthRes
) => {
config.accessToken = authRes.access_token;
config.refreshToken = authRes.refresh_token;
config.accessTokenExpiresInSeconds = authRes.expires_in;
config.accessTokenExpiresAtTime =
Date.now() + authRes.expires_in * 1000 - 10 * 1000;
config.accountID = authRes.account_id;
};
export const revokeAuth = async (client: Dropbox) => {
await client.authTokenRevoke();
};

View File

@ -18,6 +18,14 @@
display: none; display: none;
} }
.dropbox-auth-button-hide {
display: none;
}
.dropbox-revoke-auth-button-hide {
display: none;
}
.webdav-disclaimer { .webdav-disclaimer {
font-weight: bold; font-weight: bold;
} }