JWT login always returns 404 “user does not exist / not active / deleted” (Passbolt 5.4.1)

Hi everyone,
I’m trying to use JWT Authentication but I consistently receive 404:

{
  "header": {
    "id": "e9139c26-4c6f-4e52-bf7e-05d11f7401c7",
    "status": "error",
    "servertime": 1758029601,
    "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
    "message": "The user does not exist or is not active or has been deleted.",
    "url": "/auth/jwt/login.json",
    "code": 404
  },
  "body": ""
}

Server log:

2025-09-16 13:33:21 error: [Cake\Http\Exception\NotFoundException]
The user does not exist or is not active or has been deleted.
in /usr/share/php/passbolt/plugins/PassboltCe/JwtAuthentication/src/Controller/JwtLoginController.php on line 77

Environment

  • Passbolt CE 5.4.1

What I have verified

  1. User exists and is active in the database
SELECT id, username, active, deleted FROM users
WHERE id = '<USER_UUID>';
-- returns: active=1, deleted=0

SELECT user_id, fingerprint, deleted FROM gpgkeys
WHERE user_id = '<USER_UUID>';
-- returns: fingerprint = 58E98A725953255F37AF622B4B903A6E7B2AA0E6, deleted=0

  1. PGP challenge is correct (sign-then-encrypt)
  • Encrypted to the server public key from GET /auth/verify.json (recipient keyid matches).

  • Signed with the user’s private key (GnuPG shows Good signature) with fingerprint:
    58E9 8A72 5953 255F 37AF 622B 4B90 3A6E 7B2A A0E6.

  • Decrypting locally (for debugging) shows this inner JSON:

{
  "version": "1.0.0",
  "domain": "https://<my-passbolt-domain>",
  "verify_token": "9640cd11-b6ed-4246-9774-61625b1660d7",
  "verify_token_expiry": 1757705719
}

(Expiry is an integer epoch in the future; token is UUID with hyphens.)

  1. HTTP flow / CSRF
  • I request csrfToken via GET /auth/verify.json and send it back on the POST (cookie + X-CSRF-Token), with Content-Type: application/json.

  • Minimal repro:

# 1) get csrf + cookies
curl -sS -c cookies.txt https://<my-passbolt-domain>/auth/verify.json >/dev/null
CSRF=$(awk '/csrfToken/ {print $7}' cookies.txt)

# 2) send the armored PGP challenge
CH=$(jq -Rs . < challenge.asc)

# 3) POST login
curl -i -b cookies.txt \
  -H "Content-Type: application/json" \
  -H "X-CSRF-Token: $CSRF" \
  -d "{\"user_id\":\"<USER_UUID>\",\"challenge\":$CH}" \
  https://<my-passbolt-domain>/auth/jwt/login.json
# → consistently returns 404 (same message)

Ask

Given the validations above (user active, GPG key present and matching, PGP challenge valid, CSRF handled), what else should I check to understand why /auth/jwt/login.json returns 404 (FAILURE_IDENTITY_NOT_FOUND) in this scenario?

Thanks!

Hello,

The flow you are using is not correct (some vibe coding led you there maybe? :smiley: ). It’s mixing both authentication flows (GpgAuth and GpgJwtAuth).

For GpgJwtAuth authentication there is only one endpoint to call, e.g.

POST /auth/jwt/login.json
{
  "user_id": "f848277c-5398-58f8-a82a-72397af2d450"
  "challenge": "-----BEGIN PGP MESSAGE-----"
}

Where challenge is encrypted with server public key and signed with user private key:

{
  "version": "1.0.0",
  "domain": "https://passbolt.dev/workspace", // your domain
  "verify_token":"799c69c7-1789-4d87-9fbf-02529b0d21dc", // your random uuid
  "verify_token_expiry": 1622630495, // current unix time + 2min or so
}

Once the initial request has been verified, the server can return the success response. It contains the following:

{
  "header": {
    "id": "799c69c7-1789-4d87-9fbf-02529b0d21dc",
    "status": "success",
    "servertime": 1554909967,
    "action": "ad71952e-7842-599e-a19e-3a82e6974b23",
    "message": "The operation was successful.",
    "url": "\/auth\/jwt\/login.json",
    "code": 200
  },
  "body": {
    "challenge": "-----BEGIN PGP MESSAGE-----"
  }
}

The tokens are contained within the challenge returned by the login endpoint encrypted using the user OpenPGP keys. Once decrypted the challenge includes access tokens, verify token, refresh token. The access tokens are signed with the server public RSA key (see JWKS endpoint).

{
  "version": "v1.0.0",
  "domain": "https://passbolt.dev/workspace",
  "verify_token": "799c69c7-1789-4d87-9fbf-02529b0d21dc", // from previous step
  "access_token": "<JWT token>",
  "refresh_token": "899c69c7-1789-4d87-9fbf-02529b0d21dd",
}

Ref. https://docs.google.com/document/d/1KNpnKNj0c8V3McFn160GErtxmWk3Z9KFfPLV7VBCQyQ/edit?tab=t.0

When i go directly to:

POST /auth/jwt/login.json
{
  "user_id": "f848277c-5398-58f8-a82a-72397af2d450"
  "challenge": "-----BEGIN PGP MESSAGE-----"
}

i recive:

{
    "header": {
        "id": "26fae4e9-7121-4ddf-bed3-1eec394b188f",
        "status": "error",
        "servertime": 1758032328,
        "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
        "message": "Missing or incorrect CSRF cookie type.",
        "url": "/auth/jwt/login.json",
        "code": 403
    },
    "body": ""
}

I should disable CSRF cookie then?

CSRF Cookie is not needed for this endpoint

strange, because I when not set this cookie, keep return this response:

{
    "header": {
        "id": "26fae4e9-7121-4ddf-bed3-1eec394b188f",
        "status": "error",
        "servertime": 1758032328,
        "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
        "message": "Missing or incorrect CSRF cookie type.",
        "url": "/auth/jwt/login.json",
        "code": 403
    },
    "body": ""
}

I need to set any environment variable? to disable this validation?

I’m not managing to reproduce your problem do you have the full curl query you are sending with header and cookies if any?

curl --location 'https://mydomain/auth/jwt/login.json' \
--header 'Content-Type: application/json' \
--header 'Cookie: AWSALBAPP-0=_remove_; AWSALBAPP-1=_remove_; AWSALBAPP-2=_remove_; AWSALBAPP-3=_remove_; passbolt_session=1ub2vdq587721vo4hm22kpt5bo' \
--data '{"user_id":"2f57f325-dac3-444e-b759-73f762c731e0", "challenge": "\"-----BEGIN PGP MESSAGE-----\\n\\nhQGMA9\\u002B0R5V2cYciAQv/VvVvXmknUiqD6nhQy4dHPMgrzJEcT5IVoAf7y/xhFHDV\\nnTY/yeNw7yynjJV2zjse33vHNp4NvQFlqbzQp9CSKtpWPgM2c3KHr4VsJ4B5Mgwf\\naQSc1qiEkqQ4TyZX3ChJfJhjDBfVAT09BfRyAsbGdkOA4IjfWERhD1rZN20TU/Ic\\n9vRava6R40upPLe5l8vXK5Qu4C8qAX\\u002BY\\u002BL8LIdm/DzCRzkGOE9IN3NA/Nx/dbA5B\\ni8PZLXXu66iVNjwXx1dpwLwOip3TJIx0Vm7D4DZapv32LkQpkeQGgYvgphkYrUpa\\nBzCFz0897w1aa6/Z2ONGa8RtzKbou4/ZYZrxC62ca8Pbq3I8C4TkWzmXgZyi3mB6\\notPRthXcXX5e3RpnQAvEQDeH7Ff/lQ0Ql2QtzqCcP/e1ntQNlgYEzc\\u002BOmMO9gl\\u002B0\\nswfxUc2yNXqORAXg3oufv5nb1\\u002B02IEod35AVlTkCKJZUG7VY82Q0q2jODQWimhZn\\n2nIDC3qmP7YOM3ubVYc/0sCBAUfNTLb/sIlDwXyS7PAid2Y4Q5DsA6WKTOLicFK8\\nTu6TOkWOKENYxtIdkOKWITbRAkfSaATRqQovtVdHSwn6dRE3vOscuNEWnTBIq99O\\n3GXnn5opeNWDxBePDt2PkDau6uKfj0S2vKk3gdZPOEXuIhklq7pCVi49RHxaopWx\\nvANxFuCCqJjlByHTHXby8bj17X8pK1I5gOEEkXx\\u002Bs8RhbJz/xSWQ1TFTCIWbqKMW\\n\\u002BfI6h2B3QcmtWJRvm9rTz/IultJ0DEJSOAustZx0C74xVDtr\\u002B\\u002BRG7rRv0biZ3u\\u002Bl\\nyWygCuFBBLNgJrCuipK7JRlghBbF/l749b36Fq2EUEYrKgXQeEhvc78K\\u002BlEWQN9K\\nXt2K13htkTOoF8DGWwZPcD/2WzMwv//0CFphrokqXPjj0Jc/RmVJ6gcLtqgouy3V\\ns2Dy\\n=Jocl\\n-----END PGP MESSAGE-----\\n\""}'

Still not able to reproduce, it should look like this nonetheless:

curl --request POST \
  --url https://passbolt-api/auth/jwt/login.json \
  --header 'content-type: application/json' \
  --data '{"user_id": "uuid", "challenge": "-----BEGIN PGP MESSAGE-----"}'

No need to send passbolt_session cookie or any other cookie for that matter. Request should be POST.

@remy I try to use your example from another topic, and I was able to reproduce the same output.

https://community.passbolt.com/t/jwt-authentication-issues-with-api/10999/6?u=fabricio0915

(node:13185) Warning: Setting the NODE_TLS_REJECT_UNAUTHORIZED environment variable to '0' makes TLS connections and HTTPS requests insecure by disabling certificate verification.
(Use `node --trace-warnings ...` to show where the warning was created)
{
    "header": {
        "id": "7f6b1eff-c0b5-4094-bc7c-25c5ae623293",
        "status": "error",
        "servertime": 1758074821,
        "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
        "message": "Missing or incorrect CSRF cookie type.",
        "url": "\/auth\/jwt\/login.json",
        "code": 403
    },
    "body": ""
}

and when I add the X-CSRF-Token header, the output is:

{
    "header": {
        "id": "c2c69b2b-1682-4be3-bdb8-281d85026cb9",
        "status": "error",
        "servertime": 1758074879,
        "action": "28c0972b-e6a2-5d44-a5cb-bc2d11799cc1",
        "message": "The user does not exist or is not active or has been deleted.",
        "url": "/auth/jwt/login.json",
        "code": 404
    },
    "body": ""
}

Would you recommend to implement de old authentication method (the deprecated one)?

Using the go example on the Passbolt Github, I was able to connect to the Passbolt api, but these example use the old way..

@remy can you help with that point? if necessary i can provide more information or do more tests to resolve this issue.

The jwt-auth check should be before the csrf-validation (as I look at the github source).

Apparently, your login-request does not seem to be recognized as a jwt-login request.
The post you link is an example I used as well to get the jwt-login working with js/node. What does your fetch look like?

Had the same issue today. The JWT authentication plugin was disabled in my case, that was the problem.

Fix for Docker:

environment:
  PASSBOLT_PLUGINS_JWT_AUTHENTICATION_ENABLED: "true"

Restart container and you’re good.
No cookies, no csrf-token needed. Just enable the plugin and JWT login works.

For non-Docker (not 100% sure, but should work):

'passbolt' => [
    'plugins' => [
        'jwtAuthentication' => [
            'enabled' => true,
        ],
    ],
],

That’s it.

1 Like