Implement S256 challenge in OpenRouter OAuth flow (#5501)
* feat: implement S256 challenge in OpenRouter OAuth flow * fix: add error handling for missing OpenRouter authorization code * fix: save verifier to accountStorage Co-authored-by: Copilot <copilot@github.com> * fix: comment on getVerifierKey --------- Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
Generated
+7
@@ -73,6 +73,7 @@
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"is-docker": "^3.0.0",
|
||||
"isomorphic-git": "^1.36.3",
|
||||
"js-sha256": "^0.11.1",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^3.0.2",
|
||||
@@ -6366,6 +6367,12 @@
|
||||
"integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/js-sha256": {
|
||||
"version": "0.11.1",
|
||||
"resolved": "https://registry.npmjs.org/js-sha256/-/js-sha256-0.11.1.tgz",
|
||||
"integrity": "sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
|
||||
|
||||
@@ -64,6 +64,7 @@
|
||||
"ipaddr.js": "^2.2.0",
|
||||
"is-docker": "^3.0.0",
|
||||
"isomorphic-git": "^1.36.3",
|
||||
"js-sha256": "^0.11.1",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"mime-types": "^3.0.2",
|
||||
|
||||
@@ -24,6 +24,7 @@ import chalk from 'chalk';
|
||||
import yaml from 'yaml';
|
||||
import * as chevrotain from 'chevrotain';
|
||||
import { gzipSync, gzip } from 'fflate';
|
||||
import { sha256 } from 'js-sha256';
|
||||
|
||||
/**
|
||||
* Expose the libraries to the 'window' object.
|
||||
@@ -105,6 +106,7 @@ export default {
|
||||
chevrotain,
|
||||
gzipSync,
|
||||
gzip,
|
||||
sha256,
|
||||
};
|
||||
|
||||
export {
|
||||
@@ -132,4 +134,5 @@ export {
|
||||
chevrotain,
|
||||
gzipSync,
|
||||
gzip,
|
||||
sha256,
|
||||
};
|
||||
|
||||
+2
-1
@@ -213,7 +213,7 @@ import {
|
||||
tag_import_setting,
|
||||
applyCharacterTagsToMessageDivs,
|
||||
} from './scripts/tags.js';
|
||||
import { initSecrets, readSecretState } from './scripts/secrets.js';
|
||||
import { checkOpenRouterAuth, initSecrets, readSecretState } from './scripts/secrets.js';
|
||||
import { markdownExclusionExt } from './scripts/showdown-exclusion.js';
|
||||
import { markdownUnderscoreExt } from './scripts/showdown-underscore.js';
|
||||
import { NOTE_MODULE_NAME, initAuthorsNote, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from './scripts/authors-note.js';
|
||||
@@ -748,6 +748,7 @@ async function firstLoadInit() {
|
||||
await initPresetManager();
|
||||
await initSystemMessages();
|
||||
await getSettings(initLoaderHandle);
|
||||
await checkOpenRouterAuth();
|
||||
initKeyboard();
|
||||
initDynamicStyles();
|
||||
initTags();
|
||||
|
||||
+60
-13
@@ -1,5 +1,5 @@
|
||||
import { DOMPurify, moment } from '../lib.js';
|
||||
import { event_types, eventSource, getRequestHeaders } from '../script.js';
|
||||
import { DOMPurify, moment, sha256 } from '../lib.js';
|
||||
import { event_types, eventSource, getRequestHeaders, saveSettings } from '../script.js';
|
||||
import { t } from './i18n.js';
|
||||
import { chat_completion_sources } from './openai.js';
|
||||
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
|
||||
@@ -12,7 +12,9 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
import { textgen_types } from './textgen-settings.js';
|
||||
import { copyText, isTrueBoolean } from './utils.js';
|
||||
import { getCurrentUserHandle } from './user.js';
|
||||
import { copyText, isTrueBoolean, uuidv4 } from './utils.js';
|
||||
import { accountStorage } from './util/AccountStorage.js';
|
||||
|
||||
export const SECRET_KEYS = {
|
||||
HORDE: 'api_key_horde',
|
||||
@@ -417,7 +419,6 @@ export async function readSecretState() {
|
||||
secret_state = await response.json();
|
||||
updateSecretDisplay();
|
||||
updateInputDataLists();
|
||||
await checkOpenRouterAuth();
|
||||
}
|
||||
} catch {
|
||||
console.error('Could not read secrets file');
|
||||
@@ -497,6 +498,25 @@ export async function renameSecret(key, id, label) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a storage key for the PKCE code verifier for a given source.
|
||||
* @param {string} source Source for which to generate the storage key (e.g. 'openrouter')
|
||||
* @returns {string} The storage key for the PKCE code verifier for a given source.
|
||||
*/
|
||||
const getVerifierKey = (source) => `${getCurrentUserHandle()}_${source}_code_verifier`;
|
||||
|
||||
/**
|
||||
* Generates a code challenge for PKCE authentication flows.
|
||||
* @param {string} input Input secret string to generate the code challenge from.
|
||||
* @returns {string} S256 code challenge generated from the input string, encoded in base64url format.
|
||||
*/
|
||||
const generateChallenge = (input) => {
|
||||
const encoder = new TextEncoder();
|
||||
const data = encoder.encode(input);
|
||||
const hashBytes = sha256.array(data);
|
||||
return btoa(String.fromCharCode(...hashBytes)).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
|
||||
};
|
||||
|
||||
/**
|
||||
* Redirects the user to authorize OpenRouter.
|
||||
*/
|
||||
@@ -508,8 +528,15 @@ async function authorizeOpenRouter() {
|
||||
}
|
||||
}
|
||||
|
||||
// Generate a PKCE code verifier and code challenge
|
||||
const codeVerifier = uuidv4() + uuidv4();
|
||||
const codeChallenge = generateChallenge(codeVerifier);
|
||||
accountStorage.setItem(getVerifierKey('openrouter'), codeVerifier);
|
||||
await saveSettings();
|
||||
|
||||
// Redirect to OpenRouter authorization URL with the code challenge and callback URL
|
||||
const redirectUrl = new URL('/callback/openrouter', window.location.origin);
|
||||
const openRouterUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(redirectUrl.toString())}`;
|
||||
const openRouterUrl = `https://openrouter.ai/auth?callback_url=${encodeURIComponent(redirectUrl.toString())}&code_challenge=${codeChallenge}&code_challenge_method=S256`;
|
||||
location.href = openRouterUrl;
|
||||
}
|
||||
|
||||
@@ -517,16 +544,32 @@ async function authorizeOpenRouter() {
|
||||
* Checks if the OpenRouter authorization code is present in the URL, and if so, exchanges it for an API key.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function checkOpenRouterAuth() {
|
||||
export async function checkOpenRouterAuth() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const source = params.get('source');
|
||||
if (source === 'openrouter') {
|
||||
const query = new URLSearchParams(params.get('query'));
|
||||
const code = query.get('code');
|
||||
try {
|
||||
const code = query.get('code');
|
||||
if (!code) {
|
||||
throw new Error('OpenRouter authorization code not found in URL');
|
||||
}
|
||||
|
||||
const codeVerifier = accountStorage.getItem(getVerifierKey('openrouter'));
|
||||
if (!codeVerifier) {
|
||||
throw new Error('OpenRouter code verifier not found in accountStorage');
|
||||
}
|
||||
|
||||
const response = await fetch('https://openrouter.ai/api/v1/auth/keys', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ code }),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
code: code,
|
||||
code_verifier: codeVerifier,
|
||||
code_challenge_method: 'S256',
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
@@ -542,18 +585,22 @@ async function checkOpenRouterAuth() {
|
||||
|
||||
if (secret_state[SECRET_KEYS.OPENROUTER]) {
|
||||
toastr.success('OpenRouter token saved');
|
||||
// Remove the code from the URL
|
||||
const currentUrl = window.location.href;
|
||||
const urlWithoutSearchParams = currentUrl.split('?')[0];
|
||||
window.history.pushState({}, '', urlWithoutSearchParams);
|
||||
} else {
|
||||
throw new Error('OpenRouter token not saved');
|
||||
}
|
||||
} catch (err) {
|
||||
toastr.error('Could not verify OpenRouter token. Please try again.');
|
||||
return;
|
||||
console.error('OpenRouter OAuth error:', err);
|
||||
} finally {
|
||||
// Remove the code from the URL
|
||||
const currentUrl = window.location.href;
|
||||
const urlWithoutSearchParams = currentUrl.split('?')[0];
|
||||
window.history.pushState({}, '', urlWithoutSearchParams);
|
||||
}
|
||||
}
|
||||
|
||||
// Clean-up any code verifiers that might be left in accountStorage from abandoned auth flows
|
||||
accountStorage.removeItem(getVerifierKey('openrouter'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user