[QUESTION] Use with JWT from Firebase
Closed this issue ยท 8 comments
Question
I'd like to use this library to verify tokens coming from Google/Firebase. When doing that I get a Token not valid! Error: JWKS does not include keys
. I guess this is because the provided jwksUri
doesn't respond in the expected format. This is my current code :
const verifier = JwtRsaVerifier.create({
issuer: "https://securetoken.google.com/the-issuer,
audience: "the-audience",
jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com",
});
I got the url from here. Any idea what the correct URL is? Or is it possible to transform the response somehow?
The internet also gave me this url: https://www.googleapis.com/oauth2/v3/certs but this in turn gives me a Token not valid! Error: JWK for kid "7cf7f8727091e4c77aa995db60743b7dd2bb70b5" not found in the JWKS
.
Versions
Which version of aws-jwt-verify
are you using?
Are you using the library in Node.js or in the Web browser? Node.js
If Node.js, which version of Node.js are you using? (Should be at least 14) 18
If Web browser, which web browser and which version of it are you using? -
If using TypeScript, which version of TypeScript are you using? (Should be at least 4) 5.3.3
Looks like the URL you use is in fact the URL Google recommends, but is not in standard JWKS form as per spec
https://datatracker.ietf.org/doc/html/rfc7517#page-10 (which baffles me TBH)
You can work around by creating a custom JWKS fetcher, to transform the non standard JWKS to the standard upon this library fetching it.
The rough idea:
import { JwtRsaVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";
import { JsonFetcher } from "aws-jwt-verify/https";
// Use axios to do the HTTPS fetches
class CustomFetcher implements JsonFetcher {
public async fetch(uri: string) {
return fetch(uri).then(res => res.json()).then(keyMap => {
keys: Object.entries(keyMap).map((kid, x509cert) => (
{ kid, kty: "RSA", n, e } // Add logic here to parse Google's X509 certificates to extract modulus (n) and exponent (e)
))
})
}
}
const verifier = JwtRsaVerifier.create(
{
issuer: "https://securetoken.google.com/the-issuer,
audience: "the-audience",
jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com",
},
{
jwksCache: new SimpleJwksCache({
fetcher: new CustomFetcher(),
}),
}
);
Sorry that's as close as I can get you in reasonable time.
Wow! Thanks a lot for your input and the snippet.
I duplicated the code from SimpleJsonFetcher
and tried to make it work. Now I get a Token not valid! Error: Invalid signature
, which seems to come from here:
Here is what I got so far
import { NonRetryableFetchError } from "aws-jwt-verify/error";
import { JsonFetcher, fetchJson } from "aws-jwt-verify/https";
import { Json } from "aws-jwt-verify/safe-json-parse.js";
import crypto from "crypto";
type FetchRequestOptions = Record<string, unknown>;
/**
* HTTPS Fetcher for URIs with JSON body
*
* @param defaultRequestOptions - The default RequestOptions to use on individual HTTPS requests
*/
export class CustomFetcher implements JsonFetcher {
defaultRequestOptions: FetchRequestOptions;
constructor(props?: { defaultRequestOptions?: FetchRequestOptions }) {
this.defaultRequestOptions = {
timeout: 500,
responseTimeout: 1500,
...props?.defaultRequestOptions,
};
}
/**
* Execute a HTTPS request (with 1 immediate retry in case of errors)
* @param uri - The URI
* @param requestOptions - The RequestOptions to use
* @param data - Data to send to the URI (e.g. POST data)
* @returns - The response as parsed JSON
*/
public async fetch<ResultType extends Json>(
uri: string,
requestOptions?: FetchRequestOptions,
data?: Uint8Array
): Promise<ResultType> {
requestOptions = { ...this.defaultRequestOptions, ...requestOptions };
try {
const response = await fetchJson<ResultType>(uri, requestOptions, data);
const keys = Object.entries(response as object).map(([kid, x509cert]) => {
const cert = new crypto.X509Certificate(x509cert).toLegacyObject();
return { kid, kty: "RSA", use: "sig", n: cert.modulus!, e: cert.exponent! };
});
// @ts-expect-error
return { keys };
} catch (err) {
if (err instanceof NonRetryableFetchError) {
throw err;
}
// Retry once, immediately
return fetchJson<ResultType>(uri, requestOptions, data);
}
}
}
This fetcher returns something like this:
[
{
kid: '5b602e0ca1f47a8ebfd11604d9cbf06f4d45f82b',
kty: 'RSA',
use: 'sig',
n: 'B5A73BD071A2E6CC99B26F31BA765CA48E31D1BFCC1BFBA75A5D0031A88973DAB0504162A7085393678C9EE8673AB32BFF122633CFB00C2FDED399ECA89FAC073B877D0D4097218466750BFCBCAC44777B1E16CD728F3F11D48E0A6E939C4EAC13848B0ED52B6F941F3FAFFDD4341E67105533BD1C67D44BEB77E954070DDBB5394921288C62E6C6ED54029C9DF25066646C6785A5FE1462BA2F0217DAF65EF360C4CF43CCFB7CC3A939E85A4EA9773AAF808E32501456B9A92DF32EBDA5961C1C6BB7013B609C318188E3B43907EC5AC76107EB44FCA7EB58BBB1592C19562E7BC888A95FF6B3269FEFD82725FF03BA1E0FA0074ED29AD434E8FCEF662B7995',
e: '0x10001'
},
{
kid: '7cf7f8727091e4c77aa995db60743b7dd2bb70b5',
kty: 'RSA',
use: 'sig',
n: '947238EDA77EA33BD8CDD396BD194FE8E5CC353F9252CCEF93140E7399538D6681F00AB6015B2D08D9BD35C3AE3ACA74F5759B03A707D837DFE0385AC4904523D87734DD452E3B42D5E5CC5D39ED597BB717E225FFFB272F00A6F85BCF992BEB9A88C621A03C28F14FA1176ED4F230637031AF2D1BD3D4F6AD673A6968E5BFCDE56D48299CDE5633C9BF353DFD09F1B1F9DE9CCFE220A58CF0A7879F4D82E99F534B4E6735711EAF78BE450C1B51C714B34EFC99CF48E7DB40225F756D74F7BC4CFF904B60EB57AB21E19D71C425110A9E1BDEE7DAE0E75AB5C1C5F31ED6435EB421D40479399B23B195BC8F6DC5C439F668F98FC641F8A994E5DB9F619218D9',
e: '0x10001'
}
]
And from here I have no idea, what might be wrong.
I was expecting to see something like "e": "AQAB"
instead of e: '0x10001'
Based on https://stackoverflow.com/a/70023629, Can you convert from base16 to base64 and give another try?
Decimal 65537 => converts to hexadecimal 0x010001 => encodes to Base64 AQAB
65537 is commonly used as a public exponent in the RSA cryptosystem
Hi and thanks for your support.
Without going into details of weird JS/node decoding issues (see *
) I hardcode the given value AQAB
. Sadly the result is the same: Token not valid! Error: Invalid signature
*
console.log(Buffer.from("10001", "hex").toString("base64")); // => EAA=
console.log(Buffer.from("010001", "hex").toString("base64")); // => AQAB
console.log(Buffer.from("0010001", "hex").toString("base64")); // => AQAB
I also tried EAA=
and it didn't work.
Thanks, didn't know it is so easy nowadays in Node.js to parse an x509 cert.
Looks like this is the easiest way to get n and e in the right format now:
new crypto.X509Certificate(keyMap["7cf7f8727091e4c77aa995db60743b7dd2bb70b5"]).publicKey.export({format:"jwk"})
Also good idea to reuse the SimpleJsonFetcher with the retry, maybe this is easiest then:
import { JwtRsaVerifier } from "aws-jwt-verify";
import { SimpleJwksCache } from "aws-jwt-verify/jwk";
import { JsonFetcher, SimpleJsonFetcher } from "aws-jwt-verify/https";
class CustomFetcher implements JsonFetcher {
private fetcher = new SimpleJsonFetcher()
public async fetch(uri: string) {
return fetcher.fetch(uri).then(keyMap => {
keys: Object.entries(keyMap).map((kid, x509cert) => (
{ kid, kty: "RSA", ...crypto.X509Certificate(x509cert).publicKey.export({format:"jwk"}) }
))
})
}
}
const verifier = JwtRsaVerifier.create(
{
issuer: "https://securetoken.google.com/the-issuer,
audience: "the-audience",
jwksUri: "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com",
},
{
jwksCache: new SimpleJwksCache({
fetcher: new CustomFetcher(),
}),
}
);
Hi and once again thank you very much!
This time it works and this is the final code we came up with:
import { JsonFetcher, SimpleJsonFetcher } from "aws-jwt-verify/https";
import { Json } from "aws-jwt-verify/safe-json-parse";
import crypto from "crypto";
type FetchRequestOptions = Record<string, unknown>;
export class CustomFetcher implements JsonFetcher {
private fetcher = new SimpleJsonFetcher();
public async fetch<ResultType extends Json>(
uri: string,
requestOptions?: FetchRequestOptions,
data?: Uint8Array
): Promise<ResultType> {
// @ts-expect-error
return this.fetcher.fetch<ResultType>(uri, requestOptions, data).then(response => {
const keys = Object.entries(response as object).map(([kid, x509cert]) => {
return {
kid,
kty: "RSA",
use: "sig",
...new crypto.X509Certificate(x509cert).publicKey.export({ format: "jwk" }),
};
});
return { keys };
});
}
}
There is only one (very small) thing which is the @ts-expect-error
. Without it it gives a TS compiler error:
Type 'ResultType | { keys: { crv?: string | undefined; d?: string | undefined; dp?: string | undefined; dq?: string | undefined; e?: string | undefined; k?: string | undefined; ... 8 more ...; use: string; }[]; }' is not assignable to type 'ResultType'.
'ResultType | { keys: { crv?: string | undefined; d?: string | undefined; dp?: string | undefined; dq?: string | undefined; e?: string | undefined; k?: string | undefined; ... 8 more ...; use: string; }[]; }' is assignable to the constraint of type 'ResultType', but 'ResultType' could be instantiated with a different subtype of constraint 'Json'.
Type '{ keys: { crv?: string | undefined; d?: string | undefined; dp?: string | undefined; dq?: string | undefined; e?: string | undefined; k?: string | undefined; kty: string; n?: string | undefined; ... 6 more ...; use: string; }[]; }' is not assignable to type 'ResultType'.
'{ keys: { crv?: string | undefined; d?: string | undefined; dp?: string | undefined; dq?: string | undefined; e?: string | undefined; k?: string | undefined; kty: string; n?: string | undefined; ... 6 more ...; use: string; }[]; }' is assignable to the constraint of type 'ResultType', but 'ResultType' could be instantiated with a different subtype of constraint 'Json'.ts(2322)
...but I guess I can live with that. ๐
Once again: Thanks a lot for your great support!
This time it works
Nice!
Here's how you can solve the TS issue:
import { JsonFetcher, SimpleJsonFetcher } from "aws-jwt-verify/https";
import crypto from "crypto";
type GoogleJwks = Record<string, string>;
export class GoogleJwksFetcher implements JsonFetcher {
private fetcher = new SimpleJsonFetcher();
public async fetch(...params: Parameters<JsonFetcher["fetch"]>) {
return this.fetcher.fetch(...params).then((response) => {
const keys = Object.entries(response as GoogleJwks).map(
([kid, x509cert]) => {
return {
kid,
kty: "RSA",
use: "sig",
...new crypto.X509Certificate(x509cert).publicKey.export({
format: "jwk",
}),
};
}
);
return { keys };
});
}
}
Here's how you can solve the TS issue:
Great! ๐๐ป Thanks a lot!