I have a header, payload, and a public/private key. I can plug these all into JWT.io and it works as expected, but I'm struggling how to use these same variables with a node library like jsonwebtoken or other similar options. They seem to take a secret and sign a payload as far as I can see, which doesn't seem to line up with my inputs. I need to dynamically generate this token request so I must have the function in Node.
Thanks for any tips.
Have a look at the jsonwebtoken NPM package, which offers amongst other methods, a sign method:
var jwt = require('jsonwebtoken');
var privateKey = fs.readFileSync('private.key');
var payload = { foo: 'bar' };
var token = jwt.sign(payload, privateKey, { algorithm: 'RS256' });
As #jps has pointed out, you need the private key to sign and the public key to verify.
The header will be automatically generated and will include both properties (alg and typ) you have mentioned in your comment. You can add additional properties by passing them in the options.header parameter.
I'm struggling how to use these same variables with a node library
import * as jose from 'jose';
const privateKey = await jose.importPKCS8(privateKeyPEM); // private key just like on jwt.io
const jwt = await new jose.SignJWT(payload) // payload just like on jwt.io
.setProtectedHeader(header) // header just like on jwt.io
.sign(privateKey);
Of course there's more to be discovered if you need it.
Related
I have difficulty manipulating the Jose Node.JS documentation to chain the creation of a JWS and JWE. I cannot find the proper constructor for encryption. It looks like I can only encrypt a basic payload not a signed JWS.
Here is the code sample I try to fix to get something that would look like
const jws = await createJWS("myUserId");
const jwe = await encryptAsJWE(jws);
with the following methods
export const createJWS = async (userId) => {
const payload = {
}
payload['urn:userId'] = userId
// importing key from base64 encrypted secret key for signing...
const secretPkcs8Base64 = process.env.SMART_PRIVATE_KEY
const key = new NodeRSA()
key.importKey(Buffer.from(secretPkcs8Base64, 'base64'), 'pkcs8-private-der')
const privateKey = key.exportKey('pkcs8')
const ecPrivateKey = await jose.importPKCS8(privateKey, 'ES256')
const assertion = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: 'RS256' })
.setIssuer('demolive')
.setExpirationTime('5m')
.sign(ecPrivateKey)
return assertion
}
export const encryptAsJWE = async (jws) => {
// importing key similar to createJWS key import
const idzPublicKey = process.env.IDZ_PUBLIC_KEY //my public key for encryption
...
const pkcs8PublicKey = await jose.importSPKI(..., 'ES256')
// how to pass a signed JWS as parameter?
const jwe = await new jose.CompactEncrypt(jws)
.encrypt(pkcs8PublicKey)
return jwe
}
The input to the CompactEncrypt constructor needs to be a Uint8Array, so just wrapping the jws like so (new TextEncoder().encode(jws)) will allow you to move forward.
Moving forward then:
You are also missing the JWE protected header, given you likely use an EC key (based on the rest of your code) you should a) choose an appropriate EC-based JWE Key Management Algorithm (e.g. ECDH-ES) and put that as the public key import algorithm, then proceed to call .setProtectedHeader({ alg: 'ECDH-ES', enc: 'A128CBC-HS256' }) on the constructed object before calling encrypt.
Here's a full working example https://github.com/panva/jose/issues/112#issue-746919790 using a different combination of algorithms but it out to help you get the gist of it.
I have Google Sign-in working on my app: the relevant code is roughly:
var acc = await signInService.signIn();
var auth = await acc.authentication;
var token = auth.idToken;
This gives me a nice long token, which I then pass to my backend with an HTTP POST (this is working fine), and then try to verify. I have the same google-services.json file in my flutter tree and on the backend server (which is nodejs/restify). The backend code is roughly:
let creds = require('./google-services.json');
let auth = require('google-auth-library').OAuth2Client;
let client = new auth(creds.client[0].oauth_client[0].client_id);
. . .
let ticket = await client.verifyIdToken({
idToken: token,
audience: creds.client[0].oauth_client[0].client_id
});
let payload = ticket.getPayload();
This consistently returns my the error "Wrong recipient, payload audience != requiredAudience".
I have also tried registering separately with GCP console and using those keys/client_id instead, but same result. Where can I find the valid client_id that will properly verify this token?
The problem here is the client_id that is being used to create an OAuth2Client and the client_id being used as the audience in the verifyIdToken is the same. The client_id for the audience should be the client_id that was used in your frontend application to get the id_token.
Below is sample code from Google documentation.
const {OAuth2Client} = require('google-auth-library');
const client = new OAuth2Client(CLIENT_ID);
async function verify() {
const ticket = await client.verifyIdToken({
idToken: token,
audience: CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
const payload = ticket.getPayload();
const userid = payload['sub'];
// If request specified a G Suite domain:
//const domain = payload['hd'];
}
verify().catch(console.error);
And here is the link for the documentation.
Hope this helps.
Another quick solution might be change the name of your param "audience" to "requiredAudience". It works to me. If you copied the code from google, maybe the google documentation is outdated.
client.verifyIdToken({
idToken,
requiredAudience: GOOGLE_CLIENT_ID, // Specify the CLIENT_ID of the app that accesses the backend
// Or, if multiple clients access the backend:
//[CLIENT_ID_1, CLIENT_ID_2, CLIENT_ID_3]
});
It has already been mentioned above that requiredAudience works instead of audience, but I noticed requiredAudience works for both {client_id : <CLIENT_ID>} and <CLIENT_ID>. So maybe you were referencing creds.client[0].oauth_client[0] instead of creds.client[0].oauth_client[0].client_id? I have not been able to find any docs on the difference between requiredAudience and audience, however make sure you are sending just the <CLIENT_ID> instead of {client_id : <CLIENT_ID>}.
Google doc: link
verifyIdToken()'s call signature doesn't require the audience parameter. That's also stated in the changelog. So you can skip it, and it'll work. The documentation is kinda misleading on that.
It's also the reason why using requiredAudience works because it actually isn't being used by the method, so it's the same as not providing it.
I've been faceing this issue with google-auth-library version 8.7.0 and came across a workaround only if you have a single CLIENT_ID to verify.
Once you create your OAuth2Client like this:
const googleClient = new OAuth2Client(process.env.GOOGLE_CLIENT_ID);
You don't need to pass the CLIENT_ID in verifyIdToken function as it uses your googleClient object to create auth url.
In short : the token generated using below code gives the correct headers and payload(when I paste the generated token in JWT.io).
It only works when I insert the secret and press the secret encoded checkbox in jwt.io. After that I get valid token.
But var token = jwt.sign(payload, privateKEY, signOptions); this step should do the same thing I guess.
My code.
var jwt = require('jsonwebtoken');
var payload = {
"userId" : 'YYYYYYYYYYYYYYYYYYYYYYY',
"iat" : new Date().getTime(),
};
var signOptions = {
algorithm: "HS512"
};
var privateKEY = 'XXXXXXXXXXXXXXXXXXXXXXXX';
var token = jwt.sign(payload, privateKEY, signOptions);
console.log("Token :" + token);
This gives me an invalid token but when i paste that token in jwt.io I get the correct Headers and Payload.
And if I insert my secret and press the checkbox I get the correct token.
What I am I doing wrong. Thanks
When you check the checkbox on jwt.io, it base64 decodes your secret. Since you don't base64 encode your secret in your code, you shouldn't check that box on jwt.io. Both tokens are correct, but for different secrets. If you want the same token that you got from jwt.io with the box checked, you can use this:
var decodedSecret = Buffer.from(privateKEY, 'base64');
Then use that to sign your token instead of privateKEY. However, this doesn't really make sense, as your key isn't base64 encoded to begin with.
using jsonwebtoken
https://www.npmjs.com/package/jsonwebtoken
var jwt = require('jsonwebtoken');
var token = httpResponse.headers["x-authorization-bearer"].trim();
var decoded = jwt.decode(token);
console.log(token); // eyJ0eXAiOiJKV1QiLCJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.WTMwOGJudHBDTVM3Rm52clBPOGFPUQ.UbXYtb5KppbGYn3AkyOkCg.ljnC5I8q3qThn-NHY6qBqkFhSS9hNiR_pviIFB1zNVmp5Z2wOx0MON2sWRsDF__uSJ-PdI7QaM6djdflbTvKyPWbtKV6g_VDOU-lF6XKMI96BMK41mmBiJSNyDNxE5hqB4X_qWeCYMif8tf583bcKvkrxyuUTsRwvR2Xdo6yl9dyapYGhvKar2TtogOR9-jlFADfPL07ih0YjPYTo2gAWGzrVR6tNuyoRJolYd0ixon5nZ1aP5TdcbPrNcWmGfmuIfWN12BdiEtfrVYDNV7xwmNWfuxke0Uev5VAlIATg_U.1X6R6y9IK3n8NAexswUQKQ
console.log(decoded); // null
The JWT you've given has 4 . characters, but according to jwt.io:
JSON Web Tokens consist of three parts separated by dots (.), which are:
Header
Payload
Signature
Therefore, a JWT typically looks like the following.
xxxxx.yyyyy.zzzzz
The decode function in jsonwebtoken first calls out to a decode function in jws, which validates against the following RegEx:
/^[a-zA-Z0-9\-_]+?\.[a-zA-Z0-9\-_]+?\.([a-zA-Z0-9\-_]+)?$/
Given that your token contains 4 . characters, this validation fails and returns null, which propagates up to your call site.
If you are trying to instantiate the JSON Web token you wont get the unique token value.In fact you may end up with {} empty inject.
We can make use of the jsonwebtoken in below fashion.
eg. const token = jwt.sign(payload,SECRET_KEY,{algorithm,expireIn})
I've got a java service that generates json web tokens signed with RS256.
Then a service in node verifies the tokens with the public key using the jsonwebtoken module.
The problem is that some tokens work and some do not. According to jwt.io every token is correct.
This an example code with a working one and a failing one
var sanitize = function(data){
return data.replace(/\+/g, '-').replace(/\//g, '_').replace(/=*$/g, '')
}
var jwtGood = sanitize("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InBlbGF5by11c2VybmFtZTMiLCJlbWFpbCI6InBlbGF5by5yYW1vbisxMkBjbG91ZGRpc3RyaWN0LmNvbSIsInJvbGVzIjpbIlJPTEVfVVNFUiJdLCJpZCI6InVhb2V1YW9lb2FldTMiLCJpYXQiOjE0NTYxNDkwMjR9.tmvgtpuyuUiql2aYeR38kGTeQUwyb7XZr6Df2iv09_nxDn4HltHZm7Fvbj07ZQ5Hh_DmvlqZHz7EVSV6mERdjkohxf8tt9-J6NW_ftnUurCfLIzCcqEJ4xlKOzIgGsGrRd4ZUhw2hs4ZNTIscUb37csvKV-jPdSdQ-TxzuWZen4QnEUGvyg0VhdlU90TGZmpzobfpbHMQ3C0qhGRDMjghgej8zjWHbRDFRIGtAHLDbYVMiQRdI_GODIco2uSVh0_9PATSeRhFosHf3P3R4ohyBMrn9rxmBW4bQyFEMXWtsl4_PrKsdsaTtKjVQ2YuL5GjKQJqkWp6vx2vIxRabHz7w==");
var jwtBad = sanitize("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJ1c2VybmFtZSI6InBlbGF5by11c2VybmFtZSIsImVtYWlsIjpudWxsLCJyb2xlcyI6WyJST0xFX1VTRVIiXSwiaWQiOm51bGwsImlhdCI6MTQ1NjMwMTUwN30=.X_vogPRHoE-ws2DxB8Q3wlm5JCQdOvuedhUC-1BlGa9qPdg5nmAGLoLuuGmQZ9r2yUD45OqKQ8_PVd05b0gQBhlIIWtQsXMSWypN6o43noZqMG6aM-GeAK-edDg2C7zw0yGQDD1BNLKBeWc8lNPzJAqQV0il_lg6bytIeN2LMAgxj78RZro3snkXN4woe6afCefW78z3KiOIQ2qI3pcA6Kf4j9NErHwfe9BP2dnV3mXTOZ8SIds_C9JWb7nt9o6Z4oCpskmXxhRCpP4ptTS0krGKfzfhYMKj2e7uOwS1pV4MdpQBeLlhZaGn3pmG5kwl3ZzEeIANfE7N8a9LofmFsQ==");
var jsonwebtoken = require('jsonwebtoken');
var fs = require('fs');
var pubKey = fs.readFileSync('public.pem');
console.log(jwtGood);
jsonwebtoken.verify(jwtGood, pubKey,{ algorithms: ['RS256'] },function(err, decoded){
console.log(err,decoded);
});
console.log(jwtBad);
jsonwebtoken.verify(jwtBad, pubKey,{ algorithms: ['RS256'] },function(err, decoded){
console.log(err,decoded);
});
The main difference just by looking at them is the "=" character at the end of the encoded payload. Working one payload ends in "MjR9." and failing one in "wN30=."
Removed the "=" by hand (as i did with the ending ones) but, according to jwt.io, without it the token is not verified.
Tried some more sanitize functions but they didn't work.
My guess is that there is a base64 encoding issue here but I can't find it.