JWT authentication issues with API

Now that the previous authentication method is labeled as legacy we were looking at updating our services to login with the new JWT method. But i’m walking into issues and can’t seem to authenticate using JWT in NodeJS. Note that we can authenticate using openPGPkeys using the /auth/verify.json and auth/login.json paths.

I got the user_id for the post request to /auth/jwt/login.json from the users page inside passbolt, after clicking on the user and copying the id from the url.

The following is a concept of how we sign and encrypt the data:

const json = {
  version: "1.0.0",
  domain: "https://some.domain.local",
  verify_token: crypto.randomUUID(),
  verify_token_expiry: (Date.now() + (2*60*1000) / 1000)
}
const passphrase = "";

let clientPublicKeyArmored = fs.readFileSync('./public.client', {
  encoding: 'utf8'
});
let clientPrivateKeyArmored = fs.readFileSync('./private.client', {
  encoding: 'utf8'
});

let serverPublicKeyArmored = fs.readFileSync('./public.server', {
  encoding: 'utf8',
  flag: 'r'
}); // This is the public key we get from /auth/verify.json, saved for convenience

serverPublicKeyArmored = JSON.parse(serverPublicKeyArmored); // Parse the inline formatting back to a full text string

const serverPublicKey = await openpgp.readKey({ armoredKey: serverPublicKeyArmored });
  
const clientPublicKey = await openpgp.readKey({ armoredKey: clientPublicKeyArmored });
const clientPrivateKey = await openpgp.decryptKey({
    privateKey: await openpgp.readPrivateKey({ armoredKey: clientPrivateKeyArmored }),
    passphrase
});
  
let encrypted = await openpgp.encrypt({
      message: await openpgp.createMessage({ text: JSON.stringify(json), format: 'utf8' }),
      encryptionKeys: serverPublicKey,
      signingKeys: clientPrivateKey,
});

encrypted = encrypted.replace(/\n/g, '\\n'); // Inline formatting
console.log(encrypted)

As you can see this prints the signed and encrypted PGP message block. Because of URL formatting i’ve added \n for newlines so the whole message is inline formatted and can be send through curl/postman. I’ve even botched up a POC where i post a body to /auth/jwt/login.json, yet to no avail.

body example:

{
    "user_id": "a3f4ace8-88da-40db-91d9-5aea4ef3ca57",
    "challenge": "-----BEGIN PGP MESSAGE-----\n\nxA0DAQgBdkyDLj65TE8By5d1AGcDol57InZlcnNpb24iOiIxLjAuMCIsImRv\nbWFpbiI6Imh0dHBzOi8vby1wYXNzYm9sdC5jcy5sb2NhbCIsInZlcmlmeV90\nb2tlbiI6IjM5NzQ2ZTE0LTE0MDEtNDZhNy1iYzdmLWQ3ZjY3MGQ4MDA5ZCIs\nInZlcmlmeV90b2tlbl9leHBpcnkiOjE3MjgzNzc4MjJ9wsDzBAEBCAAnBYJn\nA6JeCZB2TIMuPrlMTxYhBKAxTSPcs3yHM3hbdHZMgy4+uUxPAAANeAv/albH\ngyOObN9cs6NP87zyvCP34wQE+XozuAP/n/qSK2lEfCjP8K+eyy2Ko2e8C3pF\nSZGlP8A0EcyyTPWibK4th7BsGmMC+c8wUNHIiVCTtawraugWba0dzYdhjimE\nUuUAkQnc/tsEFz4t3A7td+dTXZvDvDlMg9kAF7lZQo2tOB1k1ZI6Y0VZycOq\noiFpWfeNyie8gyEjEmIUulY+1lQqiqm5DdFGU9dQ3UzMCvtgWaPBLv/68kie\n3oyNq6/DWc7kTNnTwnhAUR4aKLIXD1WLSU7UZ/bpAhii7dM5zOn7GXNykChX\nlZjwYhnM29Vpy+n+qKLU16fvsCdh2cW1Y/KZ1PRMmxbl/9HpKTQdWxafo4dq\n3wEO0bUwvIaj8XwK9YawIx3yKQwoWVlou8dorQzRPm59KuyBST34HiT2ovkm\naUzNxwiZte/5PlI4ahqDSQsVX8M2yVcYe0ruhGMvAwSoYKqSClr+EarupTYI\nyavrks0CyPurYJItzdp19t83CuqM\n=SnsW\n-----END PGP MESSAGE-----\n"
}

The only result i ever see is:

{
    "header": {
        "id": "e92fe22e-4910-478d-ac48-741464087a23",
        "status": "error",
        "servertime": 1728295676,
        "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
        "message": "The credentials are invalid.",
        "url": "/auth/jwt/login.json",
        "code": 400
    },
    "body": ""
}

Any help is very much appreciated.

Health check has fully passed without errors:


     ____                  __          ____
    / __ \____  _____ ____/ /_  ____  / / /_
   / /_/ / __ `/ ___/ ___/ __ \/ __ \/ / __/
  / ____/ /_/ (__  |__  ) /_/ / /_/ / / /
 /_/    \__,_/____/____/_.___/\____/_/\__/

 Open source password manager for teams
-------------------------------------------------------------------------------
 Healthcheck shell
-------------------------------------------------------------------------------

 Environment

 [PASS] PHP version 8.2.20.
 [PASS] PHP version is 8.1 or above.
 [PASS] PCRE compiled with unicode support.
 [PASS] Mbstring extension is installed.
 [PASS] Intl extension is installed.
 [PASS] GD or Imagick extension is installed.
 [PASS] The temporary directory and its content are writable and not executable.
 [PASS] The logs directory and its content are writable.

 Config files

 [PASS] The application config file is present
 [PASS] The passbolt config file is present

 Core config

 [PASS] Cache is working.
 [PASS] Debug mode is off.
 [PASS] Unique value set for security.salt
 [PASS] Full base url is set to https://domain
 [PASS] App.fullBaseUrl validation OK.
 [PASS] /healthcheck/status is reachable.

 SSL Certificate

 [WARN] SSL peer certificate does not validate.
 [HELP] cURL Error (60) SSL certificate problem: self-signed certificate in certificate chain
 [WARN] Hostname does not match when validating certificates.
 [HELP] cURL Error (60) SSL certificate problem: self-signed certificate in certificate chain
 [WARN] Using a self-signed certificate.
 [HELP] Check https://help.passbolt.com/faq/hosting/troubleshoot-ssl
 [HELP] cURL Error (60) SSL certificate problem: self-signed certificate in certificate chain

 SMTP settings

 [PASS] The SMTP Settings plugin is enabled.
 [PASS] SMTP Settings coherent. You may send a test email to validate them.
 [WARN] The SMTP Settings source is: /etc/passbolt/passbolt.php.
 [HELP] It is recommended to set the SMTP Settings in the database through the administration section.
 [WARN] The SMTP Settings plugin endpoints are enabled.
 [HELP] It is recommended to disable the plugin endpoints.
 [HELP] Set the PASSBOLT_SECURITY_SMTP_SETTINGS_ENDPOINTS_DISABLED environment variable to true.
 [HELP] Or set passbolt.security.smtpSettings.endpointsDisabled to true in /etc/passbolt/passbolt.php.
 [PASS] No custom SSL configuration for SMTP server.

 JWT Authentication

 [PASS] The JWT Authentication plugin is enabled.
 [PASS] The /etc/passbolt/jwt/ directory is not writable.
 [PASS] A valid JWT key pair was found.

 GPG Configuration

 [PASS] PHP GPG Module is installed and loaded.
 [PASS] The environment variable GNUPGHOME is set to /var/lib/passbolt/.gnupg.
 [PASS] The directory /var/lib/passbolt/.gnupg containing the keyring is writable by the webserver user.
 [PASS] The server OpenPGP key is not the default one.
 [PASS] The public key file is defined in /etc/passbolt/passbolt.php and readable.
 [PASS] The private key file is defined in /etc/passbolt/passbolt.php and readable.
 [PASS] The server key fingerprint matches the one defined in /etc/passbolt/passbolt.php.
 [PASS] The server public key defined in the /etc/passbolt/passbolt.php (or environment variables) is in the keyring.
 [PASS] There is a valid email id defined for the server key.
 [PASS] The public key can be used to encrypt a message.
 [PASS] The private key can be used to sign a message.
 [PASS] The public and private keys can be used to encrypt and sign a message.
 [PASS] The private key can be used to decrypt a message.
 [PASS] The private key can be used to decrypt and verify a message.
 [PASS] The public key can be used to verify a signature.
 [PASS] The server public key format is Gopengpg compatible.
 [PASS] The server private key format is Gopengpg compatible.

 Application configuration

 [PASS] Using latest passbolt version (4.9.1).
 [PASS] Passbolt is configured to force SSL use.
 [PASS] App.fullBaseUrl is set to HTTPS.
 [PASS] Selenium API endpoints are disabled.
 [PASS] Search engine robots are told not to index content.
 [INFO] The Self Registration plugin is enabled.
 [INFO] The self registration provider is: Email domain safe list.
 [WARN] The deprecated self registration public setting was found in /etc/passbolt/passbolt.php.
 [HELP] You may remove the "passbolt.registration.public" setting.
 [WARN] Host availability checking is disabled.
 [HELP] Make sure this instance is not publicly available on the internet.
 [HELP] Or set the PASSBOLT_EMAIL_VALIDATE_MX environment variable to true.
 [HELP] Or set passbolt.email.validate.mx to true in /etc/passbolt/passbolt.php.
 [PASS] Serving the compiled version of the javascript app.
 [WARN] Some email notifications are disabled by the administrator.
 [PASS] The database schema is up to date.

 Database

 [PASS] The application is able to connect to the database
 [PASS] 31 tables found.
 [PASS] Some default content is present.

 [PASS] No error found. Nice one sparky!

Hello,

Do you have any additional information in the server logs? You should get some more information on what check is failing. I can try to reproduce later with your code, but that might be quicker checking that first.

Cheers,

Thank you for your reply.

I run passbolt in a docker container. Where the /var/log/passbolt/error.log streams into /dev/stderr. error.log → /dev/stderr

docker logs gives:

[07/Oct/2024:15:50:33 +0200] "POST /auth/jwt/login.json HTTP/1.1" 400 250 "-" "PostmanRuntime/7.42.0"
2024-10-07 15:50:33,032 INFO reaped unknown pid 10762 (exit status 0)
2024-10-07 15:50:33,032 INFO reaped unknown pid 10764 (exit status 0)
2024-10-07 15:50:33,032 INFO reaped unknown pid 10767 (exit status 0)
2024-10-07 15:50:33,032 INFO reaped unknown pid 10769 (exit status 0)
2024-10-07 15:50:33,032 INFO reaped unknown pid 10772 (exit status 0)
2024-10-07 15:50:33,032 INFO reaped unknown pid 10774 (exit status 0)
2024-10-07 15:50:33,033 INFO reaped unknown pid 10776 (exit status 0)
2024-10-07 15:50:33,033 INFO reaped unknown pid 10778 (exit status 0)

Besides code 400 this doesn’t explain to me which check is failing.

These are the nginx log not passbolt log.
I will give it a shot.

I think that’s your issue, should be

This worked for me

const openpgp = require('openpgp'); 
const fs = require('fs');
const fetch = require('node-fetch');
process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0";
const domain = 'https://localhost';
(async () => {
    const json = {
        version: "1.0.0",
        domain: domain,
        verify_token: crypto.randomUUID(),
        verify_token_expiry: ((Date.now() + (2*60*1000)) / 1000)
    }
    const passphrase = "ada@passbolt.com";

    const clientPrivateKeyArmored = fs.readFileSync('./private.client', {encoding: 'utf8',});

    const clientPrivateKey = await openpgp.decryptKey({
        privateKey: await openpgp.readPrivateKey({armoredKey: clientPrivateKeyArmored}),
        passphrase
    });

    const serverPublicKeyArmored = fs.readFileSync('./public.server', {encoding: 'utf8',});

    const serverPublicKey = await openpgp.readKey({armoredKey: serverPublicKeyArmored});

    let encrypted = await openpgp.encrypt({
        message: await openpgp.createMessage({text: JSON.stringify(json)}),
        encryptionKeys: serverPublicKey,
        signingKeys: clientPrivateKey,
    });

    const response = await fetch(domain + '/auth/jwt/login.json', {
        method: "POST",
        headers: {
            "Content-Type": "application/json",
        },
        body: JSON.stringify({
            "user_id": "f848277c-5398-58f8-a82a-72397af2d450",
            "challenge": encrypted
        })
    });
    const body = await response.text();

    console.log(body);
})();

Thank you very much for looking into this.

(Date.now() + (2*60*1000) / 1000)

Silly me.

Regardless of anything, it works. I’m pretty sure i used a correct epoch before, but i guess not.

Thank you.

My actual issue was that the verify_token_expiry can only last for 1 hour. And then later when i botched that equation. Thanks once again though.