2

I have a reactJS web app and I am using react-redux-firebase for authentification and it works fine.

Now I wanted to add a own nodejs server (with a little db behind) and I read that I can use the firebase token from the react webapp login to authenticate the user on the nodeJs server, before letting him do changes on the database.

But I get the error

No pem found for envelope: {"alg":"RS256","kid":"f5c9aebe234da6016bd7b949168b8cd5b4ec9eeb","typ":"JWT"}

This is how I get the token on client side and send it to the server


async function updateDataInDatabase2(data, dispatch, getState) {
    try {
        await axios.post(`http://localhost:5000/app/todo/data`, JSON.stringify(data), {
            headers: { 'Content-Type': 'application/json', 'firebase-idToken': getState().firebase.auth.stsTokenManager.accessToken },
        });
    } catch (err) {
        console.log(err.message);
    }
}

The token I send looks like this:

'eyJhbGciOiJSUzI1NiIsImtpZCI6ImY1YzlhZWJlMjM0ZGE2MDE2YmQ3Yjk0OTE2OGI4Y2Q1YjRlYzllZWIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vam9ycm9jaC1jb25zdWx0aW5nIiwiYXVkIjoiam9ycm9jaC1jb25zdWx0aW5nIiwiYXV0aF90aW1lIjoxNTkwMDU5Njk5LCJ1c2VyX2lkIjoiVkFXSEFZVXdXS2g5akJkbE82QWFOc0ZUVFJwMSIsInN1YiI6IlZBV0hBWVV3V0toOWpCZGxPNkFhTnNGVFRScDEiLCJpYXQiOjE1OTAwNTk2OTksImV4cCI6MTU5MDA2MzI5OSwiZW1haWwiOiJtYXJrdXNhY3Rpb25qYWNrc29uQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbIm1hcmt1c2FjdGlvbmphY2tzb25AZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.KRWI4drcSSU-3YVALR4kJlLdbB9JfhSvbhsa4nR42r41IQkeRj8uIZRkA6-kmprpohlRt8v3ZlM1NsY37eNeHsI1x-KIYlVmcpE4GDETrTMNQ-NdfoyIAFqij79iPi4b7y5uDy6uFLLiKgoQKUJKIT_OFDcM1vUXeLrsA0Jn4eoA7w8wMHTqCA0vYU2YutLuBydpfdElGM0LR3yqWAf6jJFjRu45vyY1JQsI2utTBCv21B4_IDFiN7ov8NqUFjX5CHkRNiipF9P6H9USYvcPTg6AQYfMhMd8V1rtT9_EXPZMNEMnR72sIWG9Y5-Fq4fT7K1mtj7OZlKURGfKSdyybA'

It seems to be a valid token.

I also tried to get it like that:

getFirebase().auth().currentUser.getIdToken()

but that delivers an object like this:

const token1 = {
    a: 2,
    b: null,
    c: null,
    f: null,
    g: false,
    h: false,
    i:
        'eyJhbGciOiJSUzI1NiIsImtpZCI6ImY1YzlhZWJlMjM0ZGE2MDE2YmQ3Yjk0OTE2OGI4Y2Q1YjRlYzllZWIiLCJ0eXAiOiJKV1QifQ.eyJpc3MiOiJodHRwczovL3NlY3VyZXRva2VuLmdvb2dsZS5jb20vam9ycm9jaC1jb25zdWx0aW5nIiwiYXVkIjoiam9ycm9jaC1jb25zdWx0aW5nIiwiYXV0aF90aW1lIjoxNTkwMDU5Njk5LCJ1c2VyX2lkIjoiVkFXSEFZVXdXS2g5akJkbE82QWFOc0ZUVFJwMSIsInN1YiI6IlZBV0hBWVV3V0toOWpCZGxPNkFhTnNGVFRScDEiLCJpYXQiOjE1OTAwNTk2OTksImV4cCI6MTU5MDA2MzI5OSwiZW1haWwiOiJtYXJrdXNhY3Rpb25qYWNrc29uQGdtYWlsLmNvbSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJmaXJlYmFzZSI6eyJpZGVudGl0aWVzIjp7ImVtYWlsIjpbIm1hcmt1c2FjdGlvbmphY2tzb25AZ21haWwuY29tIl19LCJzaWduX2luX3Byb3ZpZGVyIjoicGFzc3dvcmQifX0.KRWI4drcSSU-3YVALR4kJlLdbB9JfhSvbhsa4nR42r41IQkeRj8uIZRkA6-kmprpohlRt8v3ZlM1NsY37eNeHsI1x-KIYlVmcpE4GDETrTMNQ-NdfoyIAFqij79iPi4b7y5uDy6uFLLiKgoQKUJKIT_OFDcM1vUXeLrsA0Jn4eoA7w8wMHTqCA0vYU2YutLuBydpfdElGM0LR3yqWAf6jJFjRu45vyY1JQsI2utTBCv21B4_IDFiN7ov8NqUFjX5CHkRNiipF9P6H9USYvcPTg6AQYfMhMd8V1rtT9_EXPZMNEMnR72sIWG9Y5-Fq4fT7K1mtj7OZlKURGfKSdyybA',
};

Using the object might not be correct. JSON.stringyfy gives me a parsing error. So I decided to send the String behind the "i" attribute, which is the same as above.

On server side I am trying to verify it like that:

router.post('/data', async (req, res) => {
    const idToken = req.header('firebase-idToken');
    const userid = await verifyFirebaseIdToken(idToken);
    ...
    // do database modification
});



const { OAuth2Client } = require('google-auth-library');

const CLIENT_ID = 'Jorroch-Consulting-Web-App';
const client = new OAuth2Client(CLIENT_ID);
const verifyFirebaseIdToken = async (token) => {
    try {
        const ticket = await client.verifyIdToken({
            idToken: token.trim(),
            audience: CLIENT_ID, 
        });
        const payload = ticket.getPayload();
        const userid = payload['sub'];
        console.log('UserID: ', userid);
        return userid;
    } catch (error) {
        console.log('Error validating firebase idToken: ', error.message);
    }
};

But in the verifyIdToken I get the error that is thrown here in the oauth2client.js:

 if (!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) {
            // If this is not present, then there's no reason to attempt verification
            //throw new Error('No pem found for envelope: ' + JSON.stringify(envelope));
        }

Here is the whole code of the function that throws the error:

   /**
     * Verify the id token is signed with the correct certificate
     * and is from the correct audience.
     * @param jwt The jwt to verify (The ID Token in this case).
     * @param certs The array of certs to test the jwt against.
     * @param requiredAudience The audience to test the jwt against.
     * @param issuers The allowed issuers of the jwt (Optional).
     * @param maxExpiry The max expiry the certificate can be (Optional).
     * @return Returns a promise resolving to LoginTicket on verification.
     */
    async verifySignedJwtWithCertsAsync(jwt, certs, requiredAudience, issuers, maxExpiry) {
        const crypto = crypto_1.createCrypto();
        if (!maxExpiry) {
            maxExpiry = OAuth2Client.MAX_TOKEN_LIFETIME_SECS_;
        }
        const segments = jwt.split('.');
        if (segments.length !== 3) {
            throw new Error('Wrong number of segments in token: ' + jwt);
        }
        const signed = segments[0] + '.' + segments[1];
        let signature = segments[2];
        let envelope;
        let payload;
        try {
            envelope = JSON.parse(crypto.decodeBase64StringUtf8(segments[0]));
        }
        catch (err) {
            err.message = `Can't parse token envelope: ${segments[0]}': ${err.message}`;
            throw err;
        }
        if (!envelope) {
            throw new Error("Can't parse token envelope: " + segments[0]);
        }
        try {
            payload = JSON.parse(crypto.decodeBase64StringUtf8(segments[1]));
        }
        catch (err) {
            err.message = `Can't parse token payload '${segments[0]}`;
            throw err;
        }
        if (!payload) {
            throw new Error("Can't parse token payload: " + segments[1]);
        }
        if (!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) {
            // If this is not present, then there's no reason to attempt verification
            //throw new Error('No pem found for envelope: ' + JSON.stringify(envelope));
        }
        const cert = certs[envelope.kid];
        if (envelope.alg === 'ES256') {
            signature = formatEcdsa.joseToDer(signature, 'ES256').toString('base64');
        }
        const verified = await crypto.verify(cert, signed, signature);
        if (!verified) {
            throw new Error('Invalid token signature: ' + jwt);
        }
        if (!payload.iat) {
            throw new Error('No issue time in token: ' + JSON.stringify(payload));
        }
        if (!payload.exp) {
            throw new Error('No expiration time in token: ' + JSON.stringify(payload));
        }
        const iat = Number(payload.iat);
        if (isNaN(iat))
            throw new Error('iat field using invalid format');
        const exp = Number(payload.exp);
        if (isNaN(exp))
            throw new Error('exp field using invalid format');
        const now = new Date().getTime() / 1000;
        if (exp >= now + maxExpiry) {
            throw new Error('Expiration time too far in future: ' + JSON.stringify(payload));
        }
        const earliest = iat - OAuth2Client.CLOCK_SKEW_SECS_;
        const latest = exp + OAuth2Client.CLOCK_SKEW_SECS_;
        if (now < earliest) {
            throw new Error('Token used too early, ' +
                now +
                ' < ' +
                earliest +
                ': ' +
                JSON.stringify(payload));
        }
        if (now > latest) {
            throw new Error('Token used too late, ' +
                now +
                ' > ' +
                latest +
                ': ' +
                JSON.stringify(payload));
        }
        if (issuers && issuers.indexOf(payload.iss) < 0) {
            throw new Error('Invalid issuer, expected one of [' +
                issuers +
                '], but got ' +
                payload.iss);
        }
        // Check the audience matches if we have one
        if (typeof requiredAudience !== 'undefined' && requiredAudience !== null) {
            const aud = payload.aud;
            let audVerified = false;
            // If the requiredAudience is an array, check if it contains token
            // audience
            if (requiredAudience.constructor === Array) {
                audVerified = requiredAudience.indexOf(aud) > -1;
            }
            else {
                audVerified = aud === requiredAudience;
            }
            if (!audVerified) {
                throw new Error('Wrong recipient, payload audience != requiredAudience');
            }
        }
        return new loginticket_1.LoginTicket(envelope, payload);
    }

So something must be missing in the token that

!Object.prototype.hasOwnProperty.call(certs, envelope.kid)) 

is true.

In debug mode I see that certs is an object with two attributes (960a7e8e8341ed752f12b186fa129731fe0b04c0 and c1771814ba6a70693fb9412da3c6e90c2bf5b927) an the kid attribute in the envelope is f5c9aebe234da6016bd7b949168b8cd5b4ec9eeb.

So kid has the same length what lets me think that it is not completly wrong. It seems that the verify tries to verify against the certificate specified in the kid attribute but there are only two different certificates in the certs object.

Does anybode have a idea whats wrong with the token?

I am searching the whole day now and think of kicking out firebase of my project.

MarkusJackson
  • 225
  • 2
  • 12

1 Answers1

4

It seems there is a lot of complexity by using an external library like oauth2client.js, you can alternatively consider using the Firebase Admin SDK on your backend side to try and authenticate the token. That way you can send the token returned from the promise

firebase.auth().currentUser.getIdToken(true).then(idToken => axios.post...)

And on your node.js server you can setup your Admin SDK by following the following instructions: https://firebase.google.com/docs/admin/setup

And then just call the authenticate method when you receive the request

admin.auth().verifyIdToken(idToken)
    .then(function(decodedToken) {
        let uid = decodedToken.uid;
    }
Will
  • 75
  • 6
  • 2
    My gosh, I literally spent hours trying to use `google-auth-library` in Firebase Functions and faced OP's error (`No pem found for envelope`). Finally found your answer and replaced everything with `admin.auth().verifyIdToken` which just works like a charm. You deserve a cookie Will, thank you! – David Dal Busco Sep 03 '20 at 15:39
  • 2
    I got here from David's article, which deserves a link from here too https://itnext.io/firebase-cloud-functions-verify-users-tokens-d4e60e314d1a – Yura Feb 20 '21 at 23:14