mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-05-10 18:00:17 +02:00
211 lines
7.1 KiB
TypeScript
211 lines
7.1 KiB
TypeScript
import crypto from "node:crypto";
|
|
import type { Context, GetHttpAuthenticationConfigRequest, PluginDefinition } from "@yaakapp/api";
|
|
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: [
|
|
{
|
|
type: "banner",
|
|
color: "info",
|
|
inputs: [
|
|
{
|
|
type: "markdown",
|
|
content:
|
|
"OAuth 1.0 is still in beta. Please submit any issues to [Feedback](https://yaak.app/feedback).",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
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);
|
|
const first = all[0];
|
|
if (first == null) continue;
|
|
requestData.data[key] = all.length > 1 ? all : first;
|
|
}
|
|
|
|
// (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");
|
|
}
|
|
}
|