Error decoding JWT when ExpiredSignature #1374

Closed
opened 2026-02-05 00:44:58 +03:00 by OVERLORD · 9 comments
Owner

Originally created by @tessus on GitHub (Oct 4, 2022).

Subject of the issue

I have invited a user via the admin panel about 10 days ago (SIGNUPS_ALLOWED=false, SIGNUPS_VERIFY=false). Today the user tried to register, but got the following error after clicking Submit: Error decoding JWT.

I suspected that the invitation was expired and checked the log file:

[2022-10-04 17:01:28.379][response][INFO] (register) POST /api/accounts/register => 400 Bad Request
[2022-10-04 17:01:55.333][request][INFO] POST /api/accounts/register
[2022-10-04 17:01:55.335][error][ERROR] Error decoding JWT.
[CAUSE] Error(
    ExpiredSignature,
)

Then I have noticed that I can't just resend an invitation. There's no resend option in the admin panel, nor can I re-invite the user. (user already exists error message)
I had to delete the user, even though the user has not yet registered and then send a new invite.

I understand that invitations expire, although I should be able to specify how long they are valid in the config .

The error message that is presented to the user is rather cryptic and I believe something like The invitation has expired. is better than Error decoding JWT..

Since I am only guessing that ExpiredSignature means that the invitation has indeed expired, what is the timeframe a user has to use the invitation? Can we make this a config parameter? Would you accept a PR? Well, I'd have to dig into Rust and the code first. ;-)

I would not have expected for an invitation to expire at all, especially when SIGNUPS_ALLOWED=false. This is why I opened this bug report.

Update: I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization.

Deployment environment

Your environment (Generated via diagnostics page)

  • Vaultwarden version: v1.25.2
  • Web-vault version: v2022.6.2
  • Running within Docker: false (Base: Unknown)
  • Environment settings overridden: false
  • Uses a reverse proxy: true
  • IP Header check: true (X-Real-IP)
  • Internet access: true
  • Internet access via a proxy: false
  • DNS Check: true
  • Time Check: true
  • Domain Configuration Check: true
  • HTTPS Check: true
  • Database type: MySQL
  • Database version: 8.0.30
  • Clients used:
  • Reverse proxy and version:
  • Other relevant information:

Config (Generated via diagnostics page)

Show Running Config

Environment settings which are overridden:

{
  "_duo_akey": null,
  "_enable_duo": false,
  "_enable_email_2fa": true,
  "_enable_smtp": true,
  "_enable_yubico": true,
  "_icon_service_csp": "",
  "_icon_service_url": "",
  "_ip_header_enabled": true,
  "admin_ratelimit_max_burst": 3,
  "admin_ratelimit_seconds": 300,
  "admin_token": "***",
  "allowed_iframe_ancestors": "",
  "attachments_folder": "/data/system/vaultwarden/attachments",
  "authenticator_disable_time_drift": false,
  "data_folder": "/data/system/vaultwarden",
  "database_conn_init": "",
  "database_max_conns": 16,
  "database_timeout": 30,
  "database_url": "*****://*:*@*/*",
  "db_connection_retries": 15,
  "disable_2fa_remember": false,
  "disable_admin_token": true,
  "disable_icon_download": false,
  "domain": "*****://*****.********.**",
  "domain_origin": "*****://*****.********.**",
  "domain_path": "",
  "domain_set": true,
  "duo_host": null,
  "duo_ikey": null,
  "duo_skey": null,
  "email_attempts_limit": 3,
  "email_expiration_time": 600,
  "email_token_size": 6,
  "emergency_access_allowed": true,
  "emergency_notification_reminder_schedule": "0 5 * * * *",
  "emergency_request_timeout_schedule": "0 5 * * * *",
  "enable_db_wal": true,
  "extended_logging": true,
  "helo_name": null,
  "hibp_api_key": null,
  "icon_blacklist_non_global_ips": true,
  "icon_blacklist_regex": null,
  "icon_cache_folder": "/data/system/vaultwarden/icon_cache",
  "icon_cache_negttl": 259200,
  "icon_cache_ttl": 2592000,
  "icon_download_timeout": 10,
  "icon_redirect_code": 302,
  "icon_service": "internal",
  "incomplete_2fa_schedule": "30 * * * * *",
  "incomplete_2fa_time_limit": 3,
  "invitation_org_name": "Vaultwarden",
  "invitations_allowed": true,
  "ip_header": "X-Real-IP",
  "job_poll_interval_ms": 30000,
  "log_file": "/var/log/vaultwarden/vaultwarden.log",
  "log_level": "Info",
  "log_timestamp_format": "%Y-%m-%d %H:%M:%S.%3f",
  "login_ratelimit_max_burst": 10,
  "login_ratelimit_seconds": 60,
  "org_attachment_limit": null,
  "org_creation_users": "",
  "password_hints_allowed": true,
  "password_iterations": 100000,
  "reload_templates": false,
  "require_device_email": false,
  "rsa_key_filename": "/data/system/vaultwarden/rsa_key",
  "send_purge_schedule": "0 5 * * * *",
  "sends_allowed": true,
  "sends_folder": "/data/system/vaultwarden/sends",
  "show_password_hint": false,
  "signups_allowed": false,
  "signups_domains_whitelist": "",
  "signups_verify": false,
  "signups_verify_resend_limit": 6,
  "signups_verify_resend_time": 3600,
  "smtp_accept_invalid_certs": false,
  "smtp_accept_invalid_hostnames": false,
  "smtp_auth_mechanism": null,
  "smtp_debug": false,
  "smtp_explicit_tls": null,
  "smtp_from": "***********@********.**",
  "smtp_from_name": "Vaultwarden",
  "smtp_host": "********.**",
  "smtp_password": "***",
  "smtp_port": 465,
  "smtp_security": "force_tls",
  "smtp_ssl": null,
  "smtp_timeout": 15,
  "smtp_username": "*******",
  "templates_folder": "/data/system/vaultwarden/templates",
  "tmp_folder": "/data/system/vaultwarden/tmp",
  "trash_auto_delete_days": 45,
  "trash_purge_schedule": "0 5 0 * * *",
  "use_syslog": false,
  "user_attachment_limit": null,
  "web_vault_enabled": true,
  "web_vault_folder": "/usr/share/vaultwarden/web-vault",
  "websocket_address": "0.0.0.0",
  "websocket_enabled": true,
  "websocket_port": 3012,
  "yubico_client_id": null,
  "yubico_secret_key": null,
  "yubico_server": null
}

Steps to reproduce

  1. set SIGNUPS_ALLOWED=false, SIGNUPS_VERIFY=false in config
  2. invite user
  3. wait 10 days
  4. ask the user to register

Expected behaviour

user can register

Actual behaviour

error message Error decoding JWT

Originally created by @tessus on GitHub (Oct 4, 2022). ### Subject of the issue I have invited a user via the admin panel about 10 days ago (`SIGNUPS_ALLOWED=false`, `SIGNUPS_VERIFY=false`). Today the user tried to register, but got the following error after clicking `Submit`: `Error decoding JWT.` I suspected that the invitation was expired and checked the log file: ``` [2022-10-04 17:01:28.379][response][INFO] (register) POST /api/accounts/register => 400 Bad Request [2022-10-04 17:01:55.333][request][INFO] POST /api/accounts/register [2022-10-04 17:01:55.335][error][ERROR] Error decoding JWT. [CAUSE] Error( ExpiredSignature, ) ``` Then I have noticed that I can't just resend an invitation. There's no resend option in the admin panel, nor can I re-invite the user. (user already exists error message) I had to delete the user, even though the user has not yet registered and then send a new invite. I understand that invitations expire, although I should be able to specify how long they are valid in the config . The error message that is presented to the user is rather cryptic and I believe something like `The invitation has expired.` is better than `Error decoding JWT.`. Since I am only guessing that ExpiredSignature means that the invitation has indeed expired, what is the timeframe a user has to use the invitation? Can we make this a config parameter? Would you accept a PR? Well, I'd have to dig into Rust and the code first. ;-) I would not have expected for an invitation to expire at all, especially when `SIGNUPS_ALLOWED=false`. This is why I opened this bug report. Update: I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization. ### Deployment environment ### Your environment (Generated via diagnostics page) * Vaultwarden version: v1.25.2 * Web-vault version: v2022.6.2 * Running within Docker: false (Base: Unknown) * Environment settings overridden: false * Uses a reverse proxy: true * IP Header check: true (X-Real-IP) * Internet access: true * Internet access via a proxy: false * DNS Check: true * Time Check: true * Domain Configuration Check: true * HTTPS Check: true * Database type: MySQL * Database version: 8.0.30 * Clients used: * Reverse proxy and version: * Other relevant information: ### Config (Generated via diagnostics page) <details><summary>Show Running Config</summary> **Environment settings which are overridden:** ```json { "_duo_akey": null, "_enable_duo": false, "_enable_email_2fa": true, "_enable_smtp": true, "_enable_yubico": true, "_icon_service_csp": "", "_icon_service_url": "", "_ip_header_enabled": true, "admin_ratelimit_max_burst": 3, "admin_ratelimit_seconds": 300, "admin_token": "***", "allowed_iframe_ancestors": "", "attachments_folder": "/data/system/vaultwarden/attachments", "authenticator_disable_time_drift": false, "data_folder": "/data/system/vaultwarden", "database_conn_init": "", "database_max_conns": 16, "database_timeout": 30, "database_url": "*****://*:*@*/*", "db_connection_retries": 15, "disable_2fa_remember": false, "disable_admin_token": true, "disable_icon_download": false, "domain": "*****://*****.********.**", "domain_origin": "*****://*****.********.**", "domain_path": "", "domain_set": true, "duo_host": null, "duo_ikey": null, "duo_skey": null, "email_attempts_limit": 3, "email_expiration_time": 600, "email_token_size": 6, "emergency_access_allowed": true, "emergency_notification_reminder_schedule": "0 5 * * * *", "emergency_request_timeout_schedule": "0 5 * * * *", "enable_db_wal": true, "extended_logging": true, "helo_name": null, "hibp_api_key": null, "icon_blacklist_non_global_ips": true, "icon_blacklist_regex": null, "icon_cache_folder": "/data/system/vaultwarden/icon_cache", "icon_cache_negttl": 259200, "icon_cache_ttl": 2592000, "icon_download_timeout": 10, "icon_redirect_code": 302, "icon_service": "internal", "incomplete_2fa_schedule": "30 * * * * *", "incomplete_2fa_time_limit": 3, "invitation_org_name": "Vaultwarden", "invitations_allowed": true, "ip_header": "X-Real-IP", "job_poll_interval_ms": 30000, "log_file": "/var/log/vaultwarden/vaultwarden.log", "log_level": "Info", "log_timestamp_format": "%Y-%m-%d %H:%M:%S.%3f", "login_ratelimit_max_burst": 10, "login_ratelimit_seconds": 60, "org_attachment_limit": null, "org_creation_users": "", "password_hints_allowed": true, "password_iterations": 100000, "reload_templates": false, "require_device_email": false, "rsa_key_filename": "/data/system/vaultwarden/rsa_key", "send_purge_schedule": "0 5 * * * *", "sends_allowed": true, "sends_folder": "/data/system/vaultwarden/sends", "show_password_hint": false, "signups_allowed": false, "signups_domains_whitelist": "", "signups_verify": false, "signups_verify_resend_limit": 6, "signups_verify_resend_time": 3600, "smtp_accept_invalid_certs": false, "smtp_accept_invalid_hostnames": false, "smtp_auth_mechanism": null, "smtp_debug": false, "smtp_explicit_tls": null, "smtp_from": "***********@********.**", "smtp_from_name": "Vaultwarden", "smtp_host": "********.**", "smtp_password": "***", "smtp_port": 465, "smtp_security": "force_tls", "smtp_ssl": null, "smtp_timeout": 15, "smtp_username": "*******", "templates_folder": "/data/system/vaultwarden/templates", "tmp_folder": "/data/system/vaultwarden/tmp", "trash_auto_delete_days": 45, "trash_purge_schedule": "0 5 0 * * *", "use_syslog": false, "user_attachment_limit": null, "web_vault_enabled": true, "web_vault_folder": "/usr/share/vaultwarden/web-vault", "websocket_address": "0.0.0.0", "websocket_enabled": true, "websocket_port": 3012, "yubico_client_id": null, "yubico_secret_key": null, "yubico_server": null } ``` </details> ### Steps to reproduce 1. set `SIGNUPS_ALLOWED=false`, `SIGNUPS_VERIFY=false` in config 2. invite user 3. wait 10 days 4. ask the user to register ### Expected behaviour user can register ### Actual behaviour error message `Error decoding JWT`
OVERLORD added the enhancementdocumentationlow priority labels 2026-02-05 00:44:58 +03:00
Author
Owner

@BlackDex commented on GitHub (Oct 4, 2022):

The timeout is 5 days, and not configurable. We should indeed create a re-invite option or re-send option within the admin interface.

The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit.

@BlackDex commented on GitHub (Oct 4, 2022): The timeout is 5 days, and not configurable. We should indeed create a re-invite option or re-send option within the admin interface. The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit.
Author
Owner

@tessus commented on GitHub (Oct 4, 2022):

The timeout is 5 days, and not configurable.

Thanks for the info.

I've updated the first comment, so I don't know if you have seen this part: I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization.

I don't know the code, so I don't know if such a differentiation would introduce unecessary complexity.

We should indeed create a re-invite option or re-send option within the admin interface.

In that case it would also make sense to show in the admin panel that an invitation has expired. What do you think?

The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit.

True, if somebody can read the error log. A user only gets this useless message: Error decoding JWT. which tells them nothing. Wouldn't a more specific error message be more user friendly and useful?

@tessus commented on GitHub (Oct 4, 2022): > The timeout is 5 days, and not configurable. Thanks for the info. I've updated the first comment, so I don't know if you have seen this part: _I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization._ I don't know the code, so I don't know if such a differentiation would introduce unecessary complexity. > We should indeed create a re-invite option or re-send option within the admin interface. In that case it would also make sense to show in the admin panel that an invitation has expired. What do you think? > The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit. True, if somebody can read the error log. A user only gets this useless message: `Error decoding JWT.` which tells them nothing. Wouldn't a more specific error message be more user friendly and useful?
Author
Owner

@BlackDex commented on GitHub (Oct 4, 2022):

The timeout is 5 days, and not configurable.

Thanks for the info.

I've updated the first comment, so I don't know if you have seen this part: I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization.

I don't know the code, so I don't know if such a differentiation would introduce unecessary complexity.

That is difficult, since we need to use the invite flow from Bitwarden.
Under water we have a fake org which we use for this, so that is a bit chained.

We should indeed create a re-invite option or re-send option within the admin interface.

In that case it would also make sense to show in the admin panel that an invitation has expired. What do you think?

That would be useful indeed. Maybe even a link wich changes wether the link is expired or not to re-send or re-invite.

The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit.

True, if somebody can read the error log. A user only gets this useless message: Error decoding JWT. which tells them nothing. Wouldn't a more specific error message be more user friendly and useful?

In that case a better message would be a better option indeed.

@BlackDex commented on GitHub (Oct 4, 2022): > > The timeout is 5 days, and not configurable. > > Thanks for the info. > > I've updated the first comment, so I don't know if you have seen this part: _I also think there should be a difference between an admin that invites a user via the admin panel and someone inviting somebody to a new (not the deafult) organization._ > > I don't know the code, so I don't know if such a differentiation would introduce unecessary complexity. > That is difficult, since we need to use the invite flow from Bitwarden. Under water we have a fake org which we use for this, so that is a bit chained. > > We should indeed create a re-invite option or re-send option within the admin interface. > > In that case it would also make sense to show in the admin panel that an invitation has expired. What do you think? > That would be useful indeed. Maybe even a link wich changes wether the link is expired or not to re-send or re-invite. > > The ExpiredSignature kinda is descriptive enough i think, but maybe we can optimize it a bit. > > True, if somebody can read the error log. A user only gets this useless message: `Error decoding JWT.` which tells them nothing. Wouldn't a more specific error message be more user friendly and useful? In that case a better message would be a better option indeed.
Author
Owner

@tessus commented on GitHub (Oct 4, 2022):

Thanks again for the explanation. I will leave this in your capable hands.

I have never coded anything in Rust, but if there's anything I can do, please point me in the right direction.

@tessus commented on GitHub (Oct 4, 2022): Thanks again for the explanation. I will leave this in your capable hands. I have never coded anything in Rust, but if there's anything I can do, please point me in the right direction.
Author
Owner

@stefan0xC commented on GitHub (Oct 4, 2022):

I actually asked exactly a year ago over on discourse how long the invitations are valid, but then only tested if they were automatically removed not if the invitations still work.

A few days ago, I started working on my feature request from back then but it's still mostly a work in progress. I ran into a dead end yesterday because I noticed that I did not actually understand how the invitations worked internally but I think I understand now and what I have to change to make it work.

@stefan0xC commented on GitHub (Oct 4, 2022): I actually [asked exactly a year ago over on discourse](https://vaultwarden.discourse.group/t/how-long-are-invitations-kept/1166?u=stefan0xc) how long the invitations are valid, but then only tested if they were automatically removed not if the invitations still work. A few days ago, I started working on [my feature request from back then](https://vaultwarden.discourse.group/t/automatically-expire-invitations/1212/2) but it's still mostly a work in progress. I ran into a dead end yesterday because I noticed that I did not actually understand how the invitations worked internally but I think I understand now and what I have to change to make it work.
Author
Owner

@stefan0xC commented on GitHub (Oct 5, 2022):

Okay, my pull request implementing the cleanup of stale user entries is now more or less complete and ready for review. ✌️
So admin intervention should not be needed anymore (unless the invitation_purge_schedule option is configured so that the cleanup happens only at infrequent intervals).

@stefan0xC commented on GitHub (Oct 5, 2022): Okay, my pull request implementing the cleanup of stale user entries is now more or less complete and ready for review. :v: So admin intervention should not be needed anymore (unless the invitation_purge_schedule option is configured so that the cleanup happens only at infrequent intervals).
Author
Owner

@tessus commented on GitHub (Oct 6, 2022):

I think improving the error message would already help. I looked through the code, but this error message is very generic and broad.

I do not know how I would differentiate between a real decoding error (e.g. the token has the wrong format, the checksum is not correct, ...) and the fact that the decoding failed due to an expired token.

I only see a call to jsonwebtoken::decode but the error message seems to be applied to whatever error it might be.

@tessus commented on GitHub (Oct 6, 2022): I think improving the error message would already help. I looked through the code, but this error message is very generic and broad. I do not know how I would differentiate between a real decoding error (e.g. the token has the wrong format, the checksum is not correct, ...) and the fact that the decoding failed due to an expired token. I only see a call to `jsonwebtoken::decode` but the error message seems to be applied to whatever error it might be.
Author
Owner

@stefan0xC commented on GitHub (Oct 9, 2022):

I've made a PR which should fix this issue in the simplest way I could think of.

For even more user friendly messages e.g. The invitation has expired. I am not sure if it's worth the effort as the error returned by decode_jwt (or rather by jsonwebtoken::decode) would have to be bubbled up to at least each of the 7 calling functions and handled there, if not further up as the functions might be used for different purposes (e.g. decode_invite is not only used for register and accept_invite but also for list_policies_token).

@stefan0xC commented on GitHub (Oct 9, 2022): I've made a PR which should fix this issue in the simplest way I could think of. For even more user friendly messages e.g. `The invitation has expired.` I am not sure if it's worth the effort as the error returned by `decode_jwt` (or rather by `jsonwebtoken::decode`) would have to be bubbled up to at least each of the 7 calling functions and handled there, if not further up as the functions might be used for different purposes (e.g. `decode_invite` is not only used for [`register`](https://github.com/dani-garcia/vaultwarden/blob/6fa6eb18e88f6d090eff5139d5a67bb56d25bf7f/src/api/core/accounts.rs#L112) and [`accept_invite`](https://github.com/dani-garcia/vaultwarden/blob/6fa6eb18e88f6d090eff5139d5a67bb56d25bf7f/src/api/core/organizations.rs#L777) but also for [`list_policies_token`](https://github.com/dani-garcia/vaultwarden/blob/6fa6eb18e88f6d090eff5139d5a67bb56d25bf7f/src/api/core/organizations.rs#L1242)).
Author
Owner

@tessus commented on GitHub (Oct 9, 2022):

Thanks @stefan0xC for coding and thanks @dani-garcia for merging.

Even though the message does not say "invitation", I am pretty sure that users can deduct from the expired token that the invite expired. This makes it certainly a lot easier to undestand what the issue is when you do not have access to the log file on the server.

Thanks!

@tessus commented on GitHub (Oct 9, 2022): Thanks @stefan0xC for coding and thanks @dani-garcia for merging. Even though the message does not say "invitation", I am pretty sure that users can deduct from the expired token that the invite expired. This makes it certainly a lot easier to undestand what the issue is when you do not have access to the log file on the server. Thanks!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/vaultwarden#1374