mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-04-24 17:48:30 +02:00
OAuth 1 Authentication Plugin (#292)
This commit is contained in:
18
package-lock.json
generated
18
package-lock.json
generated
@@ -18,6 +18,7 @@
|
|||||||
"plugins/auth-basic",
|
"plugins/auth-basic",
|
||||||
"plugins/auth-bearer",
|
"plugins/auth-bearer",
|
||||||
"plugins/auth-jwt",
|
"plugins/auth-jwt",
|
||||||
|
"plugins/auth-oauth1",
|
||||||
"plugins/auth-oauth2",
|
"plugins/auth-oauth2",
|
||||||
"plugins/filter-jsonpath",
|
"plugins/filter-jsonpath",
|
||||||
"plugins/filter-xpath",
|
"plugins/filter-xpath",
|
||||||
@@ -4163,6 +4164,10 @@
|
|||||||
"resolved": "plugins/auth-jwt",
|
"resolved": "plugins/auth-jwt",
|
||||||
"link": true
|
"link": true
|
||||||
},
|
},
|
||||||
|
"node_modules/@yaak/auth-oauth1": {
|
||||||
|
"resolved": "plugins/auth-oauth1",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/@yaak/auth-oauth2": {
|
"node_modules/@yaak/auth-oauth2": {
|
||||||
"resolved": "plugins/auth-oauth2",
|
"resolved": "plugins/auth-oauth2",
|
||||||
"link": true
|
"link": true
|
||||||
@@ -13082,6 +13087,12 @@
|
|||||||
"node": ">= 6"
|
"node": ">= 6"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/oauth-1.0a": {
|
||||||
|
"version": "2.2.6",
|
||||||
|
"resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz",
|
||||||
|
"integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
@@ -18823,6 +18834,13 @@
|
|||||||
"@types/jsonwebtoken": "^9.0.7"
|
"@types/jsonwebtoken": "^9.0.7"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"plugins/auth-oauth1": {
|
||||||
|
"name": "@yaak/auth-oauth1",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"oauth-1.0a": "^2.2.6"
|
||||||
|
}
|
||||||
|
},
|
||||||
"plugins/auth-oauth2": {
|
"plugins/auth-oauth2": {
|
||||||
"name": "@yaak/auth-oauth2",
|
"name": "@yaak/auth-oauth2",
|
||||||
"version": "0.1.0"
|
"version": "0.1.0"
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"plugins/auth-bearer",
|
"plugins/auth-bearer",
|
||||||
"plugins/auth-jwt",
|
"plugins/auth-jwt",
|
||||||
"plugins/auth-oauth2",
|
"plugins/auth-oauth2",
|
||||||
|
"plugins/auth-oauth1",
|
||||||
"plugins/filter-jsonpath",
|
"plugins/filter-jsonpath",
|
||||||
"plugins/filter-xpath",
|
"plugins/filter-xpath",
|
||||||
"plugins/importer-curl",
|
"plugins/importer-curl",
|
||||||
|
|||||||
20
plugins/auth-oauth1/package.json
Normal file
20
plugins/auth-oauth1/package.json
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
{
|
||||||
|
"name": "@yaak/auth-oauth1",
|
||||||
|
"displayName": "OAuth 1.0",
|
||||||
|
"description": "Authenticate requests using OAuth 1.0a",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mountain-loop/yaak.git",
|
||||||
|
"directory": "plugins/auth-oauth1"
|
||||||
|
},
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"scripts": {
|
||||||
|
"build": "yaakcli build",
|
||||||
|
"dev": "yaakcli dev",
|
||||||
|
"lint": "tsc --noEmit && eslint . --ext .ts,.tsx"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"oauth-1.0a": "^2.2.6"
|
||||||
|
}
|
||||||
|
}
|
||||||
197
plugins/auth-oauth1/src/index.ts
Normal file
197
plugins/auth-oauth1/src/index.ts
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from '@yaakapp/api';
|
||||||
|
import crypto from 'node:crypto';
|
||||||
|
import OAuth from 'oauth-1.0a';
|
||||||
|
|
||||||
|
const signatures = {
|
||||||
|
HMAC_SHA1: 'HMAC-SHA1',
|
||||||
|
HMAC_SHA256: 'HMAC-SHA256',
|
||||||
|
HMAC_SHA512: 'HMAC-SHA512',
|
||||||
|
RSA_SHA1: 'RSA-SHA1',
|
||||||
|
RSA_SHA256: 'RSA-SHA256',
|
||||||
|
RSA_SHA512: 'RSA-SHA512',
|
||||||
|
PLAINTEXT: 'PLAINTEXT',
|
||||||
|
} as const;
|
||||||
|
const defaultSig = signatures.HMAC_SHA1;
|
||||||
|
|
||||||
|
const pkSigs = Object.values(signatures).filter((k) => k.startsWith('RSA-'));
|
||||||
|
const nonPkSigs = Object.values(signatures).filter((k) => !pkSigs.includes(k));
|
||||||
|
|
||||||
|
type SigMethod = (typeof signatures)[keyof typeof signatures];
|
||||||
|
|
||||||
|
function hiddenIfNot(
|
||||||
|
sigMethod: SigMethod[],
|
||||||
|
...other: ((values: GetHttpAuthenticationConfigRequest['values']) => boolean)[]
|
||||||
|
) {
|
||||||
|
return (_ctx: Context, { values }: GetHttpAuthenticationConfigRequest) => {
|
||||||
|
const hasGrantType = sigMethod.find((t) => t === String(values.signatureMethod ?? defaultSig));
|
||||||
|
const hasOtherBools = other.every((t) => t(values));
|
||||||
|
const show = hasGrantType && hasOtherBools;
|
||||||
|
return { hidden: !show };
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const plugin: PluginDefinition = {
|
||||||
|
authentication: {
|
||||||
|
name: 'oauth1',
|
||||||
|
label: 'OAuth 1.0',
|
||||||
|
shortLabel: 'OAuth 1',
|
||||||
|
args: [
|
||||||
|
{
|
||||||
|
name: 'signatureMethod',
|
||||||
|
label: 'Signature Method',
|
||||||
|
type: 'select',
|
||||||
|
defaultValue: defaultSig,
|
||||||
|
options: Object.values(signatures).map((v) => ({ label: v, value: v })),
|
||||||
|
},
|
||||||
|
{ name: 'consumerKey', label: 'Consumer Key', type: 'text', password: true, optional: true },
|
||||||
|
{
|
||||||
|
name: 'consumerSecret',
|
||||||
|
label: 'Consumer Secret',
|
||||||
|
type: 'text',
|
||||||
|
password: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tokenKey',
|
||||||
|
label: 'Access Token',
|
||||||
|
type: 'text',
|
||||||
|
password: true,
|
||||||
|
optional: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tokenSecret',
|
||||||
|
label: 'Token Secret',
|
||||||
|
type: 'text',
|
||||||
|
password: true,
|
||||||
|
optional: true,
|
||||||
|
dynamic: hiddenIfNot(nonPkSigs),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'privateKey',
|
||||||
|
label: 'Private Key (RSA-SHA1)',
|
||||||
|
type: 'text',
|
||||||
|
multiLine: true,
|
||||||
|
optional: true,
|
||||||
|
password: true,
|
||||||
|
placeholder:
|
||||||
|
'-----BEGIN RSA PRIVATE KEY-----\nPrivate key in PEM format\n-----END RSA PRIVATE KEY-----',
|
||||||
|
dynamic: hiddenIfNot(pkSigs),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'accordion',
|
||||||
|
label: 'Advanced',
|
||||||
|
inputs: [
|
||||||
|
{ name: 'callback', label: 'Callback Url', type: 'text', optional: true },
|
||||||
|
{ name: 'verifier', label: 'Verifier', type: 'text', optional: true, password: true },
|
||||||
|
{ name: 'timestamp', label: 'Timestamp', type: 'text', optional: true },
|
||||||
|
{ name: 'nonce', label: 'Nonce', type: 'text', optional: true },
|
||||||
|
{
|
||||||
|
name: 'version',
|
||||||
|
label: 'OAuth Version',
|
||||||
|
type: 'text',
|
||||||
|
optional: true,
|
||||||
|
defaultValue: '1.0',
|
||||||
|
},
|
||||||
|
{ name: 'realm', label: 'Realm', type: 'text', optional: true },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
|
||||||
|
onApply(
|
||||||
|
_ctx,
|
||||||
|
{ values, method, url },
|
||||||
|
): {
|
||||||
|
setHeaders?: { name: string; value: string }[];
|
||||||
|
setQueryParameters?: { name: string; value: string }[];
|
||||||
|
} {
|
||||||
|
const consumerKey = String(values.consumerKey || '');
|
||||||
|
const consumerSecret = String(values.consumerSecret || '');
|
||||||
|
|
||||||
|
const signatureMethod = String(values.signatureMethod || signatures.HMAC_SHA1) as SigMethod;
|
||||||
|
const version = String(values.version || '1.0');
|
||||||
|
const realm = String(values.realm || '') || undefined;
|
||||||
|
|
||||||
|
const oauth = new OAuth({
|
||||||
|
consumer: { key: consumerKey, secret: consumerSecret },
|
||||||
|
signature_method: signatureMethod,
|
||||||
|
version,
|
||||||
|
hash_function: hashFunction(signatureMethod),
|
||||||
|
realm,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (pkSigs.includes(signatureMethod)) {
|
||||||
|
oauth.getSigningKey = (tokenSecret?: string) => tokenSecret || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestUrl = new URL(url);
|
||||||
|
|
||||||
|
// Base request options passed to oauth-1.0a
|
||||||
|
const requestData: Omit<OAuth.RequestOptions, 'data'> & {
|
||||||
|
data: Record<string, string | string[]>;
|
||||||
|
} = {
|
||||||
|
method,
|
||||||
|
url: requestUrl.toString(),
|
||||||
|
includeBodyHash: false,
|
||||||
|
data: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
// (1) Include existing query params in signature base string
|
||||||
|
for (const key of requestUrl.searchParams.keys()) {
|
||||||
|
if (key.startsWith('oauth_')) continue;
|
||||||
|
const all = requestUrl.searchParams.getAll(key);
|
||||||
|
requestData.data[key] = all.length > 1 ? all : all[0]!;
|
||||||
|
}
|
||||||
|
|
||||||
|
// (2) Manual oauth_* overrides
|
||||||
|
if (values.callback) requestData.data.oauth_callback = String(values.callback);
|
||||||
|
if (values.nonce) requestData.data.oauth_nonce = String(values.nonce);
|
||||||
|
if (values.timestamp) requestData.data.oauth_timestamp = String(values.timestamp);
|
||||||
|
if (values.verifier) requestData.data.oauth_verifier = String(values.verifier);
|
||||||
|
|
||||||
|
let token: OAuth.Token | { key: string } | undefined;
|
||||||
|
|
||||||
|
if (pkSigs.includes(signatureMethod)) {
|
||||||
|
token = {
|
||||||
|
key: String(values.tokenKey || ''),
|
||||||
|
secret: String(values.privateKey || ''),
|
||||||
|
};
|
||||||
|
} else if (values.tokenKey && values.tokenSecret) {
|
||||||
|
token = { key: String(values.tokenKey), secret: String(values.tokenSecret) };
|
||||||
|
} else if (values.tokenKey) {
|
||||||
|
token = { key: String(values.tokenKey) };
|
||||||
|
}
|
||||||
|
|
||||||
|
const authParams = oauth.authorize(requestData, token as OAuth.Token | undefined);
|
||||||
|
const { Authorization } = oauth.toHeader(authParams);
|
||||||
|
return { setHeaders: [{ name: 'Authorization', value: Authorization }] };
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function hashFunction(signatureMethod: SigMethod) {
|
||||||
|
switch (signatureMethod) {
|
||||||
|
case signatures.HMAC_SHA1:
|
||||||
|
return (base: string, key: string) =>
|
||||||
|
crypto.createHmac('sha1', key).update(base).digest('base64');
|
||||||
|
case signatures.HMAC_SHA256:
|
||||||
|
return (base: string, key: string) =>
|
||||||
|
crypto.createHmac('sha256', key).update(base).digest('base64');
|
||||||
|
case signatures.HMAC_SHA512:
|
||||||
|
return (base: string, key: string) =>
|
||||||
|
crypto.createHmac('sha512', key).update(base).digest('base64');
|
||||||
|
case signatures.RSA_SHA1:
|
||||||
|
return (base: string, privateKey: string) =>
|
||||||
|
crypto.createSign('RSA-SHA1').update(base).sign(privateKey, 'base64');
|
||||||
|
case signatures.RSA_SHA256:
|
||||||
|
return (base: string, privateKey: string) =>
|
||||||
|
crypto.createSign('RSA-SHA256').update(base).sign(privateKey, 'base64');
|
||||||
|
case signatures.RSA_SHA512:
|
||||||
|
return (base: string, privateKey: string) =>
|
||||||
|
crypto.createSign('RSA-SHA512').update(base).sign(privateKey, 'base64');
|
||||||
|
case signatures.PLAINTEXT:
|
||||||
|
return (base: string) => base;
|
||||||
|
default:
|
||||||
|
return (base: string, key: string) =>
|
||||||
|
crypto.createHmac('sha1', key).update(base).digest('base64');
|
||||||
|
}
|
||||||
|
}
|
||||||
3
plugins/auth-oauth1/tsconfig.json
Normal file
3
plugins/auth-oauth1/tsconfig.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json"
|
||||||
|
}
|
||||||
@@ -247,6 +247,7 @@ function TextArg({
|
|||||||
name={arg.name}
|
name={arg.name}
|
||||||
multiLine={arg.multiLine}
|
multiLine={arg.multiLine}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
className={arg.multiLine ? 'min-h-[4rem]' : undefined}
|
||||||
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
defaultValue={value === DYNAMIC_FORM_NULL_ARG ? arg.defaultValue : value}
|
||||||
required={!arg.optional}
|
required={!arg.optional}
|
||||||
disabled={arg.disabled}
|
disabled={arg.disabled}
|
||||||
|
|||||||
@@ -91,6 +91,7 @@ export interface EditorProps {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
readOnly?: boolean;
|
readOnly?: boolean;
|
||||||
singleLine?: boolean;
|
singleLine?: boolean;
|
||||||
|
containerOnly?: boolean;
|
||||||
stateKey: string | null;
|
stateKey: string | null;
|
||||||
tooltipContainer?: HTMLElement;
|
tooltipContainer?: HTMLElement;
|
||||||
type?: 'text' | 'password';
|
type?: 'text' | 'password';
|
||||||
@@ -131,6 +132,7 @@ export function Editor({
|
|||||||
placeholder,
|
placeholder,
|
||||||
readOnly,
|
readOnly,
|
||||||
singleLine,
|
singleLine,
|
||||||
|
containerOnly,
|
||||||
stateKey,
|
stateKey,
|
||||||
type,
|
type,
|
||||||
wrapLines,
|
wrapLines,
|
||||||
@@ -540,7 +542,7 @@ export function Editor({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
if (singleLine) {
|
if (singleLine || containerOnly) {
|
||||||
return cmContainer;
|
return cmContainer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -302,6 +302,7 @@ function BaseInput({
|
|||||||
id={id.current}
|
id={id.current}
|
||||||
hideGutter
|
hideGutter
|
||||||
singleLine={!multiLine}
|
singleLine={!multiLine}
|
||||||
|
containerOnly
|
||||||
stateKey={stateKey}
|
stateKey={stateKey}
|
||||||
wrapLines={wrapLines}
|
wrapLines={wrapLines}
|
||||||
heightMode="auto"
|
heightMode="auto"
|
||||||
|
|||||||
Reference in New Issue
Block a user