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 & { data: Record; } = { 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'); } }