it didnt work
FatSecret OAuth – test against emulator
=======================================
URL:
http://localhost:5001/venus-health-and-fitness/us-central1/api/fatsecret/oauth/initiateAuth: Bearer token
(Emulator console will show [FatSecret OAuth] request/response logs.)
Response status: 500
Response body: {
"success": false,
"error": "Failed to initiate OAuth flow. Please try again.",
"code": "REQUEST_TOKEN_FAILED"
}
Request failed. Check emulator logs for [FatSecret OAuth] entries (signature, response status/data).
BELOW IS MY CODE
/**
* @fileoverview FatSecret OAuth Service
*
* Service for handling OAuth 1.0a 3-legged flow operations:
* - Request token generation
* - Access token exchange
* - Authorization URL generation
*
* @module services/fatsecret/fatsecret-oauth.service
* @requires oauth-1.0a
* @requires crypto
* @requires axios
* @requires @config/constant
* @since 1.0.0
*/
const OAuth = require('oauth-1.0a');
const crypto = require('crypto');
const axios = require('axios');
const config = require('@config/constant');
/**
* FatSecret OAuth Service Class
*/
class FatSecretOAuthService {
constructor() {
// Validate configuration
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
console.warn('[FatSecret OAuth] Warning: FatSecret credentials not configured. OAuth flow will fail.');
}
// Initialize OAuth 1.0a
this.oauth = OAuth({
consumer: {
key: config.fatsecretConsumerKey || '',
secret: config.fatsecretConsumerSecret || ''
},
signature_method: 'HMAC-SHA1',
hash_function(base_string, key) {
return crypto
.createHmac('sha1', key)
.update(base_string)
.digest('base64');
}
});
// OAuth endpoints
this.callbackUrl = config.fatsecretOAuthCallbackUrl;
// In-memory cache for request tokens (10-minute TTL)
this.requestTokenCache = new Map();
this.requestTokenTTL = 10 * 60 * 1000; // 10 minutes
}
/**
* Encode string per RFC 3986
* @param {string} str - String to encode
* @returns {string} Encoded string
*/
encodeRFC3986(str) {
return encodeURIComponent(str)
.replace(/!/g, '%21')
.replace(/'/g, '%27')
.replace(/\(/g, '%28')
.replace(/\)/g, '%29')
.replace(/\*/g, '%2A');
}
/**
* Generate OAuth signature for request token (manual implementation per FatSecret support).
* Uses raw callback URL and single encoding only to avoid signature mismatch from double encoding.
* Do not include oauth_token in request token phase.
*
* @param {string} callbackUrl - OAuth callback URL (raw, unencoded)
* @returns {Object} Authorization header
*/
generateRequestTokenSignature(callbackUrl) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}
const consumerKey = config.fatsecretConsumerKey.trim();
const consumerSecret = config.fatsecretConsumerSecret.trim();
if (!callbackUrl || typeof callbackUrl !== 'string') {
throw new Error('Invalid callback URL: must be a non-empty string');
}
try {
new URL(callbackUrl);
} catch (error) {
throw new Error(`Invalid callback URL format: ${error.message}`);
}
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
// Use RAW callbackUrl; normalization will encode it exactly once (per FatSecret)
const params = {
oauth_callback: callbackUrl,
oauth_consumer_key: consumerKey,
oauth_nonce: nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: timestamp,
oauth_version: '1.0'
};
const sortedKeys = Object.keys(params).sort();
// Normalized params: each name and value encoded once (RFC 5849 3.4.1.3.2 "encoded normalized request parameters string")
const normalizedParams = sortedKeys
.map(key => `${this.encodeRFC3986(key)}=${this.encodeRFC3986(String(params[key]))}`)
.join('&');
// Base string: method and URI are percent-encoded; third part is the normalized params string as-is (no second encoding)
const encodedMethod = this.encodeRFC3986('POST');
const encodedUrl = this.encodeRFC3986(this.requestTokenUrl);
const baseString = `${encodedMethod}&${encodedUrl}&${normalizedParams}`;
const signingKey = `${consumerSecret}&`;
const signature = crypto
.createHmac('sha1', signingKey)
.update(baseString)
.digest('base64');
const finalCallbackForHeader = this.encodeRFC3986(callbackUrl);
const authParams = [
`oauth_callback="${finalCallbackForHeader}"`,
`oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
`oauth_nonce="${this.encodeRFC3986(nonce)}"`,
`oauth_signature="${this.encodeRFC3986(signature)}"`,
`oauth_signature_method="HMAC-SHA1"`,
`oauth_timestamp="${timestamp}"`,
`oauth_version="1.0"`
Authorization: `OAuth ${authParams.join(', ')}`
};
if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Request token signature (manual, single-encode)', {
callbackUrl,
hasAuthHeader: true,
authHeaderPreview: authHeader.Authorization ? authHeader.Authorization.substring(0, 180) + '...' : 'none'
});
}
return authHeader;
}
/**
* Generate OAuth signature for access token exchange
* @param {string} requestToken - Request token
* @param {string} requestTokenSecret - Request token secret
* @param {string} verifier - OAuth verifier
* @returns {Object} Authorization header
*/
generateAccessTokenSignature(requestToken, requestTokenSecret, verifier) {
// Validate credentials exist before using
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}
const consumerKey = config.fatsecretConsumerKey.trim();
const consumerSecret = config.fatsecretConsumerSecret.trim();
const timestamp = Math.floor(Date.now() / 1000).toString();
const nonce = crypto.randomBytes(16).toString('hex');
const params = {
oauth_consumer_key: consumerKey,
oauth_token: requestToken,
oauth_nonce: nonce,
oauth_signature_method: 'HMAC-SHA1',
oauth_timestamp: timestamp,
oauth_version: '1.0',
oauth_verifier: verifier
};
// Sort parameters
const sortedKeys = Object.keys(params).sort();
const normalizedParams = sortedKeys
.map(key => `${this.encodeRFC3986(key)}=${this.encodeRFC3986(String(params[key]))}`)
.join('&');
// Build signature base string
const encodedMethod = this.encodeRFC3986('POST');
const encodedUrl = this.encodeRFC3986(this.accessTokenUrl);
const encodedParams = this.encodeRFC3986(normalizedParams);
const baseString = `${encodedMethod}&${encodedUrl}&${encodedParams}`;
// Build signing key (consumer_secret&request_token_secret) - MUST use RAW secrets, NOT encoded
// According to OAuth 1.0a spec, signing key uses raw secret values
const signingKey = `${consumerSecret}&${requestTokenSecret}`;
// Generate signature
const signature = crypto
.createHmac('sha1', signingKey)
.update(baseString)
.digest('base64');
// Build Authorization header
const authParams = [
`oauth_consumer_key="${this.encodeRFC3986(consumerKey)}"`,
`oauth_token="${this.encodeRFC3986(requestToken)}"`,
`oauth_nonce="${this.encodeRFC3986(nonce)}"`,
`oauth_signature="${this.encodeRFC3986(signature)}"`,
`oauth_signature_method="HMAC-SHA1"`,
`oauth_timestamp="${timestamp}"`,
`oauth_version="1.0"`,
`oauth_verifier="${this.encodeRFC3986(verifier)}"`
];
return {
Authorization: `OAuth ${authParams.join(', ')}`
};
}
/**
* Parse OAuth response (query string format)
* @param {string} responseText - Response text
* @returns {Object} Parsed parameters
*/
parseOAuthResponse(responseText) {
const params = {};
const pairs = responseText.split('&');
for (const pair of pairs) {
const [key, value] = pair.split('=');
if (key && value) {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
}
return params;
}
/**
* Initiate OAuth flow - get request token
* @param {string} userId - Firebase Auth user ID
* @returns {Promise<Object>} Request token and authorization URL
*/
async initiateOAuthFlow(userId) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured. Please set FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET environment variables.');
}
if (!this.callbackUrl) {
throw new Error('OAuth callback URL not configured. Please set FATSECRET_OAUTH_CALLBACK_URL environment variable.');
}
try {
// Generate signature and authorization header (per FatSecret support guidance)
const authHeader = this.generateRequestTokenSignature(this.callbackUrl);
console.log('[FatSecret OAuth] Making request token request');
console.log('[FatSecret OAuth] URL:', this.requestTokenUrl);
console.log('[FatSecret OAuth] Auth Header:', authHeader);
// Request request token (per FatSecret support: data null, only Authorization header, maxRedirects 0)
const response = await axios({
method: 'POST',
url: this.requestTokenUrl,
data: null,
headers: authHeader,
maxRedirects: 0,
timeout: 30000,
validateStatus: (status) => status < 500
});
console.log('[FatSecret OAuth] Response status:', response.status);
console.log('[FatSecret OAuth] Response data:', response.data);
if (response.status >= 300 && response.status < 400) {
const location = response.headers?.location || '';
console.error('[FatSecret OAuth] Redirect received (signature or callback issue). Location:', location);
throw new Error(`FatSecret redirected (${response.status}) to: ${location}. Fix signature (single-encode callback only) or callback URL.`);
}
// Check if response is HTML (error page)
if (typeof response.data === 'string' && (response.data.includes('<html') || response.data.includes('<!DOCTYPE') || response.data.includes('error'))) {
// Log response for debugging
if (process.env.NODE_ENV !== 'production') {
console.error('[FatSecret OAuth] HTML error response received', {
status: response.status,
responsePreview: typeof response.data === 'string' ? response.data.substring(0, 500) : response.data
});
}
throw new Error('FatSecret returned an error page. This usually means: 1) Callback URL not registered in FatSecret app settings, 2) Invalid OAuth signature, or 3) Invalid credentials. Please check your FatSecret app configuration.');
}
// Check response status
if (response.status >= 400) {
throw new Error(`FatSecret API returned error status ${response.status}. Please check your callback URL is registered in FatSecret app settings and your credentials are valid.`);
}
// Parse response
const params = this.parseOAuthResponse(response.data);
if (!params.oauth_token || !params.oauth_token_secret) {
// Check if response contains error information
if (typeof response.data === 'string' && response.data.includes('error')) {
throw new Error(`FatSecret API error: ${response.data}. Please verify your callback URL is registered in FatSecret app settings.`);
}
throw new Error('Invalid response from FatSecret: missing oauth_token or oauth_token_secret. Please check your callback URL is registered in FatSecret app settings.');
}
if (params.oauth_callback_confirmed !== 'true') {
throw new Error('OAuth callback not confirmed by FatSecret');
}
const requestToken = params.oauth_token;
const requestTokenSecret = params.oauth_token_secret;
// Store request token in cache (10-minute TTL)
this.requestTokenCache.set(requestToken, {
requestTokenSecret,
userId,
expiresAt: Date.now() + this.requestTokenTTL
});
// Generate authorization URL
// Per FatSecret support: Ensure callback URL consistency between request token and access token exchange
// Use the same callback URL that was used in the request token signature
const authorizationUrl = `${this.authorizeUrl}?oauth_token=${this.encodeRFC3986(requestToken)}&oauth_callback=${this.encodeRFC3986(this.callbackUrl)}`;
if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Request token obtained', {
userId,
requestToken: requestToken.substring(0, 10) + '...',
expiresIn: this.requestTokenTTL / 1000
});
}
return {
requestToken,
requestTokenSecret,
authorizationUrl,
expiresIn: this.requestTokenTTL / 1000
};
} catch (error) {
console.error('[FatSecret OAuth] Request failed:', error.message);
console.error('[FatSecret OAuth] Failed to get request token', {
userId,
error: error.message,
response: error.response?.data,
status: error.response?.status,
statusText: error.response?.statusText
});
if (error.response) {
console.error('[FatSecret OAuth] Response status:', error.response.status);
console.error('[FatSecret OAuth] Response data:', error.response.data);
if (error.response.status >= 300 && error.response.status < 400 && error.response.headers?.location) {
console.error('[FatSecret OAuth] Redirect location:', error.response.headers.location);
}
const status = error.response.status;
const data = error.response.data;
// Check if response is HTML error page
if (typeof data === 'string' && (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('error/500'))) {
throw new Error('FatSecret returned an error page (500). Common causes:\n' +
'1. Callback URL not registered in FatSecret app settings\n' +
'2. Invalid OAuth signature (check consumer key/secret)\n' +
'3. Callback URL mismatch (must match exactly in FatSecret app)\n' +
'4. Invalid credentials\n\n' +
'Please verify your callback URL is registered in your FatSecret developer account at: https://platform.fatsecret.com/ (Sign in → Developers → Your Application → OAuth Settings)')
; }
if (status === 401) {
throw new Error('FatSecret API authentication failed. Please check:\n' +
'1. FATSECRET_CONSUMER_KEY and FATSECRET_CONSUMER_SECRET are correct\n' +
'2. Credentials are from FatSecret Platform API (not legacy API)\n' +
'3. Credentials are active and not expired');
} else if (status === 400) {
throw new Error('Invalid OAuth request (400). Please check:\n' +
'1. Callback URL is registered in FatSecret app settings\n' +
'2. Callback URL matches exactly (including http/https, port, path)\n' +
'3. OAuth signature is correct');
} else if (status === 500) {
throw new Error('FatSecret API server error (500). This usually means:\n' +
'1. Callback URL not registered in FatSecret app settings\n' +
'2. Invalid OAuth signature\n' +
'3. Server-side issue with FatSecret\n\n' +
'Please verify your callback URL in your FatSecret developer account at: https://platform.fatsecret.com/ (Sign in → Developers → Your Application → OAuth Settings)')
; } else if (status >= 300 && status < 400) {
const location = error.response?.headers?.location || '';
throw new Error(`FatSecret redirected (${status}) to: ${location}. This often indicates an error page (e.g. /error/500). Check signature generation and callback URL.`);
} else if (status >= 400) {
throw new Error(`FatSecret API returned error ${status}. Response: ${typeof data === 'string' ? data.substring(0, 200) : JSON.stringify(data)}`);
}
}
// Network or other errors
if (error.code === 'ECONNABORTED') {
throw new Error('FatSecret API request timeout. Please try again.');
} else if (error.code === 'ENOTFOUND' || error.code === 'ECONNREFUSED') {
throw new Error('Unable to connect to FatSecret API. Please check your internet connection.');
}
throw new Error(`Failed to get request token: ${error.message}`);
}
}
/**
* Exchange request token for access token
* @param {string} requestToken - Request token
* @param {string} verifier - OAuth verifier
* @param {string} userId - Firebase Auth user ID
* @returns {Promise<Object>} Access token and token secret
*/
async exchangeAccessToken(requestToken, verifier, userId) {
if (!config.fatsecretConsumerKey || !config.fatsecretConsumerSecret) {
throw new Error('FatSecret API credentials not configured.');
}
// Retrieve request token secret from cache
const cached = this.requestTokenCache.get(requestToken);
if (!cached) {
throw new Error('Request token not found or expired. Please initiate OAuth flow again.');
}
if (cached.expiresAt < Date.now()) {
this.requestTokenCache.delete(requestToken);
throw new Error('Request token expired. Please initiate OAuth flow again.');
}
if (cached.userId !== userId) {
throw new Error('Request token does not match user. Please initiate OAuth flow again.');
}
const requestTokenSecret = cached.requestTokenSecret;
try {
// Generate OAuth signature
const authHeader = this.generateAccessTokenSignature(requestToken, requestTokenSecret, verifier);
// Exchange for access token
const response = await axios({
method: 'POST',
url: this.accessTokenUrl,
headers: {
...authHeader,
'Content-Type': 'application/x-www-form-urlencoded'
},
timeout: 30000,
validateStatus: (status) => status < 500
});
// Check if response is HTML (error page)
if (typeof response.data === 'string' && (response.data.includes('<html') || response.data.includes('<!DOCTYPE') || response.data.includes('error'))) {
throw new Error('FatSecret returned an error page during token exchange. Please verify your request token and verifier are valid.');
}
// Check response status
if (response.status >= 400) {
throw new Error(`FatSecret API returned error status ${response.status} during token exchange.`);
}
// Parse response
const params = this.parseOAuthResponse(response.data);
if (!params.oauth_token || !params.oauth_token_secret) {
if (typeof response.data === 'string' && response.data.includes('error')) {
throw new Error(`FatSecret API error during token exchange: ${response.data}`);
}
throw new Error('Invalid response from FatSecret: missing oauth_token or oauth_token_secret');
}
// Clear request token from cache
this.requestTokenCache.delete(requestToken);
if (process.env.NODE_ENV !== 'production') {
console.log('[FatSecret OAuth] Access token obtained', {
userId,
accessToken: params.oauth_token.substring(0, 10) + '...'
});
}
return {
oauthToken: params.oauth_token,
oauthTokenSecret: params.oauth_token_secret
};
} catch (error) {
console.error('[FatSecret OAuth] Failed to exchange access token', {
userId,
error: error.message,
response: error.response?.data
});
// Clear request token from cache on error
this.requestTokenCache.delete(requestToken);
if (error.response) {
const status = error.response.status;
const data = error.response.data;
// Check if response is HTML error page
if (typeof data === 'string' && (data.includes('<html') || data.includes('<!DOCTYPE') || data.includes('error'))) {
throw new Error('FatSecret returned an error page during token exchange. Please verify:\n' +
'1. Request token is valid and not expired\n' +
'2. OAuth verifier is correct\n' +
'3. Request token matches the one from initiation');
}
if (status === 401) {
throw new Error('Invalid OAuth verifier or request token. Please initiate OAuth flow again.');
} else if (status === 400) {
throw new Error('Invalid OAuth request (400). Request token may be expired or invalid. Please initiate OAuth flow again.');
} else if (status >= 400) {
throw new Error(`FatSecret API returned error ${status} during token exchange.`);
}
}
throw new Error(`Failed to exchange access token: ${error.message}`);
}
}
}
// Export singleton instance
module.exports = new FatSecretOAuthService();