Node-jsonwebtoken: AWS Load Balancer Auth

Created on 24 Aug 2018  路  11Comments  路  Source: auth0/node-jsonwebtoken

AWS recently added the functionality to authenticate a user on the load balancer and have a authenticated and hydrated user details in the request header.

I wasn't able to decode the object that comes from the load balancer even though it will decode on jwt.io. The example AWS give is in python but should be straight forward enough to decode the token.

https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html

Has anyone attempted to decode the x-amzn-oidc-data header using jwt.decode?

Most helpful comment

@daerion I found a way to make this work, but it's not pretty......

Basically, the verify method in this library won't work, but the signature can be verified using the underlying node-jwa library. Then you just have to check things like is the token still valid (I am only checking if token is not expired):

const base64Url = require("base64url");
const jwt = require("jsonwebtoken");
const jws = require("jws");
const fetch = require("node-fetch").default;

async function verifyToken(token) {
  var base64UrlToken = base64Url.fromBase64(token);
  const decoded = jwt.decode(base64UrlToken, { complete: true });

  const { kid, signer } = decoded.header;
  const region = signer.split(":")[3];

  const uri = `https://public-keys.auth.elb.${region}.amazonaws.com/${kid}`;

  console.log(`Fetching key at: ${uri}`);

  const response = await fetch(uri);
  const key = await response.text();

  console.log(key);

  try {
    const verify = jws.verify(token, "ES256", key);
    if (!verify) {
      return null;
    }

    var clockTimestamp = Math.floor(Date.now() / 1000);
    if (clockTimestamp >= decoded.header.exp) {
      // Token expired.
      return null;
    }
  } catch (err) {
    console.error(err);
    throw err;
  }

  return decoded.payload;
}

All 11 comments

Interesting!

Could you provide the code that you tried that did not work?

Off the top of my head it should be pretty easy, and would be something along the lines of:

// this should always work
console.log(jwt.decode(token));

const pubKey = fs.readFileSync('/path/to/publickey'); // replace this line with the HTTP request to get the key

jwt.verify(token, pubKey, (err, decoded) => {
  console.log('err:', err);
  console.log(decoded);
});

@MitMaro

This is what I was trying to do but I've even tried your example and it still didn't work. The only thing I added was the { algorithms: [ 'ES256' ]}.

const verifyJwt = async (token, db) => {
  const encoded_jwt_header = token.split('.')[0];
  const decoded_jwt = JSON.parse(base64.decode(encoded_jwt_header));
  const request = await fetch(
    `https://public-keys.auth.elb.us-west-2.amazonaws.com/${decoded_jwt.kid}`,
  );
  const cert = await request.text();  // -----BEGIN PUBLIC KEY----...

  return new Promise((resolve, reject) => {
    jwt.verify(
      token,
      cert,
      { algorithms: ['ES256'] }, 
      async (err, decoded) => {
        if (err) {
          reject(err);
        }

        resolve(decoded);
      },
    );
  });
};

Could you provide the output/error that occurs from the code you have above?

For reference, the following is an example the works with a ECDSA + P-256 + SHA256 public/private pair, partially taken from the tests:

'use strict';

const jwt = require('jsonwebtoken');

const es256PrivateKey = '-----BEGIN EC PARAMETERS-----\n' +
  'MIH3AgEBMCwGByqGSM49AQECIQD/////AAAAAQAAAAAAAAAAAAAAAP//////////\n' +
  '/////zBbBCD/////AAAAAQAAAAAAAAAAAAAAAP///////////////AQgWsY12Ko6\n' +
  'k+ez671VdpiGvGUdBrDMU7D2O848PifSYEsDFQDEnTYIhucEk2pmeOETnSa3gZ9+\n' +
  'kARBBGsX0fLhLEJH+Lzm5WOkQPJ3A32BLeszoPShOUXYmMKWT+NC4v4af5uO5+tK\n' +
  'fA+eFivOM1drMV7Oy7ZAaDe/UfUCIQD/////AAAAAP//////////vOb6racXnoTz\n' +
  'ucrC/GMlUQIBAQ==\n' +
  '-----END EC PARAMETERS-----\n' +
  '-----BEGIN EC PRIVATE KEY-----\n' +
  'MIIBaAIBAQQgeg2m9tJJsnURyjTUihohiJahj9ETy3csUIt4EYrV+J2ggfowgfcC\n' +
  'AQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAAAAAAAAAAAAAA////////////////\n' +
  'MFsEIP////8AAAABAAAAAAAAAAAAAAAA///////////////8BCBaxjXYqjqT57Pr\n' +
  'vVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSdNgiG5wSTamZ44ROdJreBn36QBEEE\n' +
  'axfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5RdiYwpZP40Li/hp/m47n60p8D54W\n' +
  'K84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA//////////+85vqtpxeehPO5ysL8\n' +
  'YyVRAgEBoUQDQgAEEWluurrkZECnq27UpNauq16f9+5DDMFJZ3HV43Ujc3tcXQ++\n' +
  'N1T/0CAA8ve286f32s7rkqX/pPokI/HBpP5p3g==\n' +
  '-----END EC PRIVATE KEY-----\n';

const es256PublicKey = '-----BEGIN PUBLIC KEY-----\n' +
  'MIIBSzCCAQMGByqGSM49AgEwgfcCAQEwLAYHKoZIzj0BAQIhAP////8AAAABAAAA\n' +
  'AAAAAAAAAAAA////////////////MFsEIP////8AAAABAAAAAAAAAAAAAAAA////\n' +
  '///////////8BCBaxjXYqjqT57PrvVV2mIa8ZR0GsMxTsPY7zjw+J9JgSwMVAMSd\n' +
  'NgiG5wSTamZ44ROdJreBn36QBEEEaxfR8uEsQkf4vOblY6RA8ncDfYEt6zOg9KE5\n' +
  'RdiYwpZP40Li/hp/m47n60p8D54WK84zV2sxXs7LtkBoN79R9QIhAP////8AAAAA\n' +
  '//////////+85vqtpxeehPO5ysL8YyVRAgEBA0IABBFpbrq65GRAp6tu1KTWrqte\n' +
  'n/fuQwzBSWdx1eN1I3N7XF0PvjdU/9AgAPL3tvOn99rO65Kl/6T6JCPxwaT+ad4=\n' +
  '-----END PUBLIC KEY-----\n';

async function main() {
  const token = await new Promise((resolve, reject) => {
    jwt.sign({foo: 'bar'}, es256PrivateKey,  { algorithm: 'ES256'}, (e, t) => {
      if (e) {
        return reject(e);
      }
      resolve(t);
    });
  });

  console.log('token:', token);

  console.log('decoded:', jwt.decode(token, {complete: true}));

  const verified = await new Promise((resolve, reject) => {
    jwt.verify(
      token,
      es256PublicKey,
      { algorithms: ['ES256'] },
      async (err, decoded) => {
        if (err) {
          reject(err);
        }

        resolve(decoded);
      },
    );
  });

  console.log('verified:', verified);
}

main()
  .catch((err) => console.log(err))
;

@MitMaro

Same data added to jwt.io

@MitMaro figured it out and found this:
https://github.com/brianloveswords/node-jws/pull/84

That's a common mistake but Amazon should know better. Should be easy to do a simple string replace to work around the problem.

@delianides @MitMaro Could you guys clarify the solution a bit more? What exactly are you string replacing the non url encoded bits?

@jreeter ,

Amazon is using base64 to encode the tokens, which has a different character set than base64url, which is what the JWT specification requires.

Specifically, the + character is replaced with -, the / character is replaced with _ and the trailing =s are optional. You can do a basic string replace on the invalid tokens to make them valid base64url encoded tokens.

Untested Code, but the basic idea would be:

const correctToken = token.replace(/+/g, '-').replace(/\//g, '_');

Just wanted to add that we're having the same issue. Converting token encoding to base64url will allow the library to decode it, however, it will also cause signature verification to fail (both with the library as well as on jwt.io, while using the original token and pubkey will successfully validate on jwt.io).

@daerion I found a way to make this work, but it's not pretty......

Basically, the verify method in this library won't work, but the signature can be verified using the underlying node-jwa library. Then you just have to check things like is the token still valid (I am only checking if token is not expired):

const base64Url = require("base64url");
const jwt = require("jsonwebtoken");
const jws = require("jws");
const fetch = require("node-fetch").default;

async function verifyToken(token) {
  var base64UrlToken = base64Url.fromBase64(token);
  const decoded = jwt.decode(base64UrlToken, { complete: true });

  const { kid, signer } = decoded.header;
  const region = signer.split(":")[3];

  const uri = `https://public-keys.auth.elb.${region}.amazonaws.com/${kid}`;

  console.log(`Fetching key at: ${uri}`);

  const response = await fetch(uri);
  const key = await response.text();

  console.log(key);

  try {
    const verify = jws.verify(token, "ES256", key);
    if (!verify) {
      return null;
    }

    var clockTimestamp = Math.floor(Date.now() / 1000);
    if (clockTimestamp >= decoded.header.exp) {
      // Token expired.
      return null;
    }
  } catch (err) {
    console.error(err);
    throw err;
  }

  return decoded.payload;
}

@morganabel Thanks for sharing your implementation :)

Was this page helpful?
0 / 5 - 0 ratings