-
Notifications
You must be signed in to change notification settings - Fork 350
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature request — Runtime APIs “nodejs_compat” breaks when needing crypto.createSign #3172
Comments
This is not a bug as the current behavior is intentional. |
Ok great, I imagine this would be a very useful feature. 👍 I was really loving the Cloudflare Workers DX (soo good and smooth) but unfortunately had to switch over to AWS Lambda for this. |
I'm also experiencing the same issue here |
This issue prevents the use of firebase auth or the firebase-admin SDK on Cloudflare pages,
|
I managed to avoid errors by implementing the functionality without using the googleapis library. Implementation code (click to expand)google.tsimport { decodeBase64, encodeBase64, encodeBase64Url } from "./base64";
/**
* Class for handling Google OAuth2.0 authentication
* @see {@link https://developers.google.com/identity/protocols/oauth2}
*/
export class GoogleAuth {
private readonly clientEmail: string;
private readonly privateKey: string;
private token: string | null = null;
private tokenExpiry = 0;
constructor(clientEmail: string, privateKey: string) {
this.clientEmail = clientEmail;
this.privateKey = privateKey.replace(/\\n/g, "\n");
}
async getAccessToken(): Promise<string> {
// Reuse token if still valid
if (this.token && this.tokenExpiry > Date.now()) {
return this.token;
}
// Create JWT
const now = Math.floor(Date.now() / 1000);
const expiry = now + 3600; // 1 hour later
const header = {
alg: "RS256",
typ: "JWT",
};
const claim = {
iss: this.clientEmail,
// I would suggest making the "scope" configurable as a parameter instead of having it hardcoded.
scope: "https://www.googleapis.com/auth/spreadsheets.readonly",
aud: "https://oauth2.googleapis.com/token",
exp: expiry,
iat: now,
};
try {
// Base64 encode
const encodedHeader = encodeBase64Url(header);
const encodedClaim = encodeBase64Url(claim);
// Create signature
const signatureInput = `${encodedHeader}.${encodedClaim}`;
const key = await this.importPrivateKey();
const signature = await this.signContent(signatureInput, key);
// Assemble JWT
const jwt = `${encodedHeader}.${encodedClaim}.${signature}`;
// Get access token
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: new URLSearchParams({
grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
assertion: jwt,
}),
});
if (!response.ok) {
throw new Error("Failed to get access token");
}
const data: {
access_token: string;
expires_in: number;
} = await response.json();
this.token = data.access_token;
this.tokenExpiry = Date.now() + data.expires_in * 1000;
return this.token;
} catch (err) {
console.error("getAccessToken error:", err);
throw err;
}
}
private async importPrivateKey(): Promise<CryptoKey> {
// Extract private key from PEM format
const pemContent = this.privateKey
.trim()
.split(/[\r\n]+/)
.map((line) => line.trim())
.filter(
(line) =>
!line.startsWith("-----BEGIN") && !line.startsWith("-----END"),
)
.join("");
try {
// Base64 decode
const binaryDer = decodeBase64(pemContent);
// Import PKCS8 format private key
return await crypto.subtle.importKey(
"pkcs8",
binaryDer,
{
name: "RSASSA-PKCS1-v1_5",
hash: { name: "SHA-256" },
},
false,
["sign"],
);
} catch (err) {
console.error("Private key import error:", err);
console.error("PEM content length:", this.privateKey.length);
throw err;
}
}
private async signContent(content: string, key: CryptoKey): Promise<string> {
const encoder = new TextEncoder();
const data = encoder.encode(content);
try {
const signature = await crypto.subtle.sign(
{
name: "RSASSA-PKCS1-v1_5",
hash: { name: "SHA-256" },
},
key,
data,
);
return encodeBase64(signature);
} catch (err) {
console.error("Content signing error:", err);
throw err;
}
}
} base64.tsexport function encodeBase64(data: ArrayBuffer | ArrayBufferLike): string {
return btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(data))))
.replace(/\+/g, "-")
.replace(/\//g, "_")
.replace(/=+$/, "");
}
export function decodeBase64(str: string): ArrayBuffer {
// Normalize Base64 string
const normalizedBase64 = str
.replace(/-/g, "+")
.replace(/_/g, "/")
.replace(/\s+/g, "");
// Add padding
const pad = normalizedBase64.length % 4;
const paddedBase64 = pad
? normalizedBase64 + "=".repeat(4 - pad)
: normalizedBase64;
try {
// Base64 decode
const binaryString = atob(paddedBase64);
const bytes = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
bytes[i] = binaryString.charCodeAt(i);
}
return bytes.buffer;
} catch (error) {
console.error("Base64 decode error:", error);
console.error("Input base64 length:", str.length);
throw error;
}
}
export function encodeBase64Url(obj: Record<string, unknown>): string {
const str = JSON.stringify(obj);
const encoder = new TextEncoder();
const data = encoder.encode(str);
return encodeBase64(data.buffer);
} Usage (Google Sheets)import type { ParsedEnv } from "~/constants";
import { APIError } from "~/handler";
import { GoogleAuth } from "~/utils/google";
export namespace GoogleSheetsService {
export async function getInfo(
props: ParsedEnv["googleSheets"],
): Promise<string> {
const { clientEmail, privateKey, spreadsheetId, range } = props;
const today = new Date();
const sheetName = `${today.getMonth() + 1}`;
try {
/**
* Error: [unenv] crypto.createSign is not implemented yet!
* @description Cloudflare Workers with the "nodejs_compat" flag cannot use crypto.createSign yet.
* @link https://github.com/cloudflare/workerd/issues/3172
*
* @todo Use "googleapis" library when the issue is resolved.
*/
// const auth = new google.auth.GoogleAuth({
// credentials: {
// client_email: clientEmail,
// private_key: privateKey.replace(/\\n/g, "\n"),
// },
// scopes: ["https://www.googleapis.com/auth/spreadsheets.readonly"],
// });
// const sheets = google.sheets({ version: "v4", auth });
// const response = await sheets.spreadsheets.values.get({
// spreadsheetId,
// range: `${sheetName}!${range}`,
// });
// return formatMessage(response.data.values);
/** @todo Temporary implementation */
const auth = new GoogleAuth(clientEmail, privateKey);
const accessToken = await auth.getAccessToken();
const response = await fetch(
`https://sheets.googleapis.com/v4/spreadsheets/${spreadsheetId}/values/${encodeURIComponent(`${sheetName}!${range}`)}`,
{
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json",
},
},
);
if (!response.ok) {
throw new Error(`Google Sheets API error: ${response.status}`);
}
const data: {
range: string;
values: string[][] | null | undefined;
} = await response.json();
return formatMessage(data.values);
} catch (err) {
console.error(err);
throw new APIError("Failed to fetch google sheets data", 500);
}
}
} Note This is an alternative approach and not directly related to the feature request in the title. |
It appears that this is being worked on in #3603. Not sure though what the release timeline looks like. |
The goal is to land #3603 this week with release to production hopefully next week or the week after. There are limitations to be aware of tho! Because we are using boringssl+fips the algorithms, key lengths, etc are limited. The sign/verify is known to work with RSA and ED25519 keys but DSA is unsupported. ED448 is unsupported. And there's still a bit more to do to support ECDSA, which I'll be working to verify as a next step. |
@jasnell any updates here? |
Yes! There are some limitations. RSA is supported. RSA-PSS and DSA are not. Most of the limitations are due to our use of Boringssl + FIPS in production, both of which apply additional constraints on the algorithms and parameters that are allowed. But the APIs are there! Let me know if you run into anything that just doesn't work and we'll try to get a fix in as soon as possible. |
Going to close this issue now, but will continue to watch. If y'all have any issues with the implemented APIs just comment here and mention me and I'll see it. |
Does this take time to roll out? Do I need to update any packages? I'm still getting the error referenced above #3172 (comment) using Pages / Honox / Vite setup. |
Yes, it will likely take a bit of time. If you are using wrangler, then your worker is likely still bundling the polyfill implementation that raises the "not implemented" error. The polyfill will end up taking precedence over the built-in implementation. Once wrangler is updated (which typically lags slightly behind the runtime) then it will switch to using the built-in impl. I believe there are ways of setting up your configuration now tho in advance of that but I'm not familiar enough with wrangler itself to say. I'd have to leave that to folks like @vicb to comment on. |
The new crypto API will be available when both:
Our policy is not to update wrangler until the changes are available in prod (prod updates are async with workerd NPM releases). ETA is mid-next week (Look for the unenv update in the wrangler changelog) |
The nodejs_compat does not work with crypto.createSign beause the version of
unenv
has not implemented it yet.I am trying to use the google sheets api. While this worked just fine with Firebase Functions on Node, it fails to work with Cloudflare Workers while using the "nodejs_compat" flag.
As you can see from this error message the problem is that it is relying on unenv for compatability, and this version library has not yet implemented
crypto.createSign
Using googleapis v. 144.0.0 and hono v. 4.6.12 on MacOS
Results in the following error:
The text was updated successfully, but these errors were encountered: