mastra-ai/mastra · error
Google service account private key signing failed (${(err as
Error message
Google service account private key signing failed (${(err as Error).message}). Key has BEGIN marker: ${hasBegin}, END marker: ${hasEnd}. Ensure your .env value contains the raw PEM with \n for newlines, without extra surrounding quotes or commas. What it means
This error is thrown when Node's crypto createSign('RSA-SHA256') fails to sign the JWT assertion with the Google service account private key. The library wraps the underlying crypto error and inspects the key for PEM BEGIN/END markers to diagnose the most common cause: a malformed key string. It almost always indicates the private key was mangled by environment-variable loading (unescaped newlines, extra quotes, or a JSON-embedded key with '\n' literals not converted).
Source
Thrown at auth/google/src/rbac-provider.ts:220
const header = { alg: 'RS256', typ: 'JWT', ...(account.privateKeyId ? { kid: account.privateKeyId } : {}) };
const claim = {
iss: account.clientEmail,
scope: (account.scopes ?? DEFAULT_DIRECTORY_SCOPES).join(' '),
aud: OAUTH_TOKEN_URL,
exp: now + 3600,
iat: now,
...(account.subject ? { sub: account.subject } : {}),
};
const unsigned = `${this.base64Url(JSON.stringify(header))}.${this.base64Url(JSON.stringify(claim))}`;
const privateKey = this.normalizePrivateKey(account.privateKey);
let signature: string;
try {
signature = createSign('RSA-SHA256').update(unsigned).sign(privateKey, 'base64url');
} catch (err) {
const hasBegin = privateKey.includes('-----BEGIN');
const hasEnd = privateKey.includes('-----END');
throw new Error(
`Google service account private key signing failed (${(err as Error).message}). ` +
`Key has BEGIN marker: ${hasBegin}, END marker: ${hasEnd}. ` +
`Ensure your .env value contains the raw PEM with \\n for newlines, without extra surrounding quotes or commas.`,
);
}
const response = await fetch(OAUTH_TOKEN_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
grant_type: 'urn:ietf:params:oauth:grant-type:jwt-bearer',
assertion: `${unsigned}.${signature}`,
}),
signal: AbortSignal.timeout(DEFAULT_FETCH_TIMEOUT_MS),
});
if (!response.ok) {
throw new Error(`Google service account token request failed (${response.status}): ${await response.text()}`);View on GitHub (pinned to 75dd419e61)
Solutions
- Convert literal '\n' sequences to real newlines before use: privateKey.replace(/\\n/g, '\n')
- Remove any surrounding single/double quotes and trailing commas from the env value
- Store the key as a file and read it with fs.readFileSync(keyPath, 'utf8') instead of an env var
- Regenerate/download the service account key and verify it starts with '-----BEGIN PRIVATE KEY-----'
Example fix
// before
const privateKey = process.env.GOOGLE_PRIVATE_KEY; // contains literal \n
sign({ privateKey });
// after
const privateKey = (process.env.GOOGLE_PRIVATE_KEY ?? '').replace(/\\n/g, '\n');
sign({ privateKey }); Defensive patterns
Strategy: validation
Validate before calling
const raw = process.env.GOOGLE_PRIVATE_KEY ?? '';
const privateKey = raw.includes('\\n') ? raw.replace(/\\n/g, '\n') : raw;
if (!privateKey.startsWith('-----BEGIN PRIVATE KEY-----') || !privateKey.trimEnd().endsWith('-----END PRIVATE KEY-----')) {
throw new Error('GOOGLE_PRIVATE_KEY is not valid PEM; check quoting and \\n escapes');
} Type guard
function isValidPemKey(key: string): boolean {
return typeof key === 'string'
&& key.startsWith('-----BEGIN')
&& key.includes('-----END')
&& !key.includes('\\n');
} Try / catch
try {
await provider.getToken();
} catch (err) {
if (err instanceof Error && err.message.includes('private key signing failed')) {
console.error('Service account key is malformed:', err.message);
}
throw err;
} Prevention
- Store the key in a file and read it via fs.readFileSync instead of env vars when possible
- Use secret managers (GCP Secret Manager, AWS Secrets Manager) that preserve newlines
- Normalize with .replace(/\\n/g, '\n') at config-load time in one place
- Never wrap the env value in extra quotes or copy the trailing comma from JSON
When it happens
Trigger: Calling getToken() -> getServiceAccountToken() when the GOOGLE_SERVICE_ACCOUNT key (from options or env) is not valid PEM: newlines are literal '\n' instead of real newlines, the value is wrapped in extra quotes, a trailing comma was copied from a JSON key file, or the key material itself is corrupt/truncated.
Common situations: Deploying to environments (Docker, serverless, CI) where .env values are not multiline-safe; pasting the private_key field straight from a downloaded service-account JSON into a single-line env var; dotenv versions that strip or mis-handle quoted multiline values.
Related errors
- Neon Auth base URL is required, please provide it in the opt
- Clerk JWKS URI, secret key and publishable key are required,
- Cookie password must be at least 32 characters for SSO. Set
- Redirect URI is required for SSO login
- Invalid state redirect suffix
AI-assisted analysis of mastra-ai/mastra@75dd419e61 (2026-08-30).
Data as JSON: /api/errors/ec534876f978aab7.
Report an issue: GitHub.