Normalize redirect URI to fix OAuth invalid_grant (1.2.3)

TickTick's OAuth server requires byte-exact match between the redirect_uri
the plugin sends and the value registered in the developer portal. Users
who register the URI with a trailing slash (common on iOS paste, or by
hand) get rejected with:

  error="invalid_grant"
  error_description="Invalid redirect: ...vercel.app does not match one
  of the registered values: [...vercel.app/]"

(Reported in issue #2.)

Normalize the redirect URI by stripping trailing slashes and whitespace at
all four use/save sites:

- main.ts: startAuthFlow() and exchangeAuthCodeForToken() clean the URI
  before sending — protects existing users with a slash already saved
- settings.ts: both Redirect URI inputs strip on save so the persisted
  value matches what's displayed
- settings.ts: both descriptions now warn against trailing slashes
- README: new Troubleshooting entry for the invalid_grant error

https://claude.ai/code/session_01V2fsiatACzjUCNwSEhwER9
This commit is contained in:
Claude 2026-06-05 16:16:26 +00:00
parent c036f5ab67
commit 0c25564337
No known key found for this signature in database
7 changed files with 13 additions and 10 deletions

View file

@ -114,6 +114,7 @@ After triggering, you'll see a notice confirming the task was created.
- **"TickTick auth failed: state mismatch"** — the OAuth flow expired or was started in a different session. Tap Connect again and complete the flow without delays.
- **"Failed to obtain access token"** — most often a bad Client ID or Secret, or the Redirect URI you set in the TickTick developer portal doesn't exactly match the one in the plugin. Copy/paste both rather than typing.
- **`"invalid_grant"` / `"Invalid redirect: ... does not match one of the registered values"`** — the URI registered in the TickTick Developer Portal isn't byte-equal to the one the plugin sends. The most common cause is a trailing `/` in the portal entry. Edit your TickTick app and remove any trailing slash so it matches the plugin's Redirect URI field exactly.
- **The Connect popup link does nothing** — make sure your default browser is set and you've allowed Obsidian to open external links. Try long-pressing the link to copy it, then paste into Safari/Chrome.
- **Callback page shows the code as text instead of returning to Obsidian** — that's the manual-fallback case. Paste the code into the "Authorization code" field; the plugin will exchange it for a token. (This happens if the callback page hasn't been deployed with the deep-link redirect.)
- **The "Open in Obsidian" link in TickTick does nothing** — confirm the Advanced URI plugin is installed and enabled in the same vault.

File diff suppressed because one or more lines are too long

View file

@ -236,7 +236,7 @@ export default class TickTickPlugin extends Plugin {
new Notice('Please enter your Client ID in the settings.');
return;
}
const redirectUri = this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app';
const redirectUri = (this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app').trim().replace(/\/+$/, '');
const scope = encodeURIComponent('tasks:read tasks:write');
const { codeVerifier, codeChallenge } = await generatePKCECodes();
const state = generateRandomString(32);
@ -253,7 +253,7 @@ export default class TickTickPlugin extends Plugin {
*/
async exchangeAuthCodeForToken(code: string): Promise<void> {
const tokenEndpoint = 'https://ticktick.com/oauth/token';
const redirectUri = this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app';
const redirectUri = (this.settings.redirectUri || 'https://ticktick-quick-add-obsidian-6yawfmvnj-mooshs-projects-0635287d.vercel.app').trim().replace(/\/+$/, '');
const clientId = this.settings.clientId;
const clientSecret = this.settings.clientSecret;
const codeVerifier = this.settings.tempCodeVerifier;

View file

@ -1,7 +1,7 @@
{
"id": "ticktick-quickadd-task",
"name": "TickTick Quick Add Task",
"version": "1.2.2",
"version": "1.2.3",
"minAppVersion": "1.4.0",
"description": "Create TickTick tasks from text at your cursor with automatic deep links back to your notes. Requires the Advanced URI plugin.",
"author": "Muxin Li",

View file

@ -1,6 +1,6 @@
{
"name": "ticktick-quickadd-task",
"version": "1.2.2",
"version": "1.2.3",
"description": "Create TickTick tasks from text at your cursor with automatic deep links back to your notes. Requires the Advanced URI plugin.",
"main": "main.js",
"scripts": {

View file

@ -89,13 +89,13 @@ export class TickTickSettingTab extends PluginSettingTab {
new Setting(containerEl)
.setName('Redirect URI')
.setDesc('Copy this exact value into your TickTick app\'s Redirect URI field in the developer portal. Only change it here if you self-host the callback page.')
.setDesc('Copy this exact value into your TickTick app\'s Redirect URI field in the developer portal. Do not add a trailing slash — the URIs must match exactly. Only change it here if you self-host the callback page.')
.addText(text =>
text
.setPlaceholder(DEFAULT_SETTINGS.redirectUri!)
.setValue(this.plugin.settings.redirectUri || '')
.onChange(async (value) => {
this.plugin.settings.redirectUri = value.trim();
this.plugin.settings.redirectUri = value.trim().replace(/\/+$/, '');
await this.plugin.saveSettings();
})
);
@ -241,11 +241,12 @@ export class TickTickSettingTab extends PluginSettingTab {
new Setting(details)
.setName('Redirect URI')
.setDesc('Must match the value registered in your TickTick app exactly, with no trailing slash.')
.addText(text =>
text
.setValue(this.plugin.settings.redirectUri || '')
.onChange(async (value) => {
this.plugin.settings.redirectUri = value.trim();
this.plugin.settings.redirectUri = value.trim().replace(/\/+$/, '');
await this.plugin.saveSettings();
})
);

View file

@ -3,5 +3,6 @@
"1.1.0": "1.4.0",
"1.2.0": "1.4.0",
"1.2.1": "1.4.0",
"1.2.2": "1.4.0"
"1.2.2": "1.4.0",
"1.2.3": "1.4.0"
}