snapshot of https://github.com/mountain-loop/yaak.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Lint and Test / Lint/Test (push) Has been cancelled
Generate Sponsors README / deploy (push) Has been skipped

This commit is contained in:
upstream-mirror
2026-09-23 17:39:41 +05:00
commit 906fb231cc
1544 changed files with 164131 additions and 0 deletions

View File

@@ -0,0 +1,20 @@
{
"name": "@yaak/auth-oauth1",
"displayName": "OAuth 1.0",
"version": "0.1.0",
"private": true,
"description": "Authenticate requests using OAuth 1.0a",
"repository": {
"type": "git",
"url": "https://github.com/mountain-loop/yaak.git",
"directory": "plugins/auth-oauth1"
},
"scripts": {
"build": "yaakcli build",
"dev": "yaakcli dev",
"test": "vp test --run tests"
},
"dependencies": {
"oauth-1.0a": "^2.2.6"
}
}

View File

@@ -0,0 +1,217 @@
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 } | { secret: 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) };
} else if (values.tokenSecret) {
// The secret still joins the signing key without an access token;
// leaving `key` out keeps oauth_token out of the header entirely
token = { secret: String(values.tokenSecret) };
}
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:
// RFC 5849 3.4.4: the PLAINTEXT signature IS the signing key,
// `encoded(consumer secret)&encoded(token secret)`. Returning the base
// string put the whole percent-encoded request into oauth_signature.
return (_base: string, key: string) => key;
default:
return (base: string, key: string) =>
crypto.createHmac("sha1", key).update(base).digest("base64");
}
}

View File

@@ -0,0 +1,57 @@
import { describe, expect, test } from "vite-plus/test";
import { plugin } from "../src";
function sign(values: Record<string, string>): string {
const result = plugin.authentication!.onApply!(
{} as never,
{
values,
method: "GET",
url: "https://api.example.com/resource",
} as never,
) as { setHeaders: { name: string; value: string }[] };
const header = result.setHeaders[0]!.value;
const match = header.match(/oauth_signature="([^"]*)"/);
return decodeURIComponent(match![1]!);
}
describe("PLAINTEXT signature", () => {
const base = {
signatureMethod: "PLAINTEXT",
consumerKey: "ck",
consumerSecret: "cs",
nonce: "abc123",
timestamp: "1700000000",
};
// RFC 5849 3.4.4: the PLAINTEXT signature is the signing key itself --
// encoded(consumer secret) "&" encoded(token secret) -- not the base string.
test("is the signing key, not the signature base string", () => {
expect(sign({ ...base, tokenKey: "tk", tokenSecret: "ts" })).toBe("cs&ts");
});
test("keeps the trailing separator when there is no token secret", () => {
expect(sign(base)).toBe("cs&");
});
test("includes the token secret without an access token, omitting oauth_token", () => {
const result = plugin.authentication!.onApply!(
{} as never,
{
values: { ...base, tokenSecret: "ts" },
method: "GET",
url: "https://api.example.com/resource",
} as never,
) as { setHeaders: { name: string; value: string }[] };
const header = result.setHeaders[0]!.value;
const match = header.match(/oauth_signature="([^"]*)"/);
expect(decodeURIComponent(match![1]!)).toBe("cs&ts");
expect(header).not.toContain("oauth_token=");
});
test("percent-encodes reserved characters in the secrets", () => {
expect(sign({ ...base, consumerSecret: "c s", tokenKey: "tk", tokenSecret: "t&s" })).toBe(
"c%20s&t%26s",
);
});
});

View File

@@ -0,0 +1,3 @@
{
"extends": "../../tsconfig.json"
}