Timshel/vaultwarden

refresh_token changes since 1.30.1-6 seem to cause "session expired" after 1 hour (Unifi Identity Enterprise as SSO provider)

ReqX opened this issue ยท 9 comments

ReqX commented

Subject of the issue

Unifi Identity Enterprise - SSO refresh_token "session expired" after 1 hour since 1.30.1-6

Deployment environment

posting last working version - issue can be reproduced with everything > 1.30.1.-5 (tested of course with latest version as well)

Your environment (Generated via diagnostics page)

  • Vaultwarden version: v1.30.1-5
  • Web-vault version: voidc_override-6abf9c6
  • OS/Arch: linux/x86_64
  • Running within Docker: true (Base: Debian)
  • Environment settings overridden: false
  • Uses a reverse proxy: true
  • IP Header check: true (X-Forwarded-For)
  • Internet access: true
  • Internet access via a proxy: false
  • DNS Check: true
  • Browser/Server Time Check: true
  • Server/NTP Time Check: true
  • Domain Configuration Check: true
  • HTTPS Check: true
  • Database type: SQLite
  • Database version: 3.44.0
  • 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": true,
  "_enable_email_2fa": true,
  "_enable_smtp": true,
  "_enable_yubico": true,
  "_icon_service_csp": "",
  "_icon_service_url": "",
  "_ip_header_enabled": true,
  "_smtp_img_src": "cid:",
  "admin_ratelimit_max_burst": 3,
  "admin_ratelimit_seconds": 300,
  "admin_session_lifetime": 20,
  "admin_token": "***",
  "allowed_iframe_ancestors": "",
  "attachments_folder": "data/attachments",
  "auth_request_purge_schedule": "30 * * * * *",
  "authenticator_disable_time_drift": false,
  "data_folder": "data",
  "database_conn_init": "",
  "database_max_conns": 10,
  "database_timeout": 30,
  "database_url": "***************",
  "db_connection_retries": 15,
  "disable_2fa_remember": false,
  "disable_admin_token": false,
  "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_change_allowed": true,
  "email_expiration_time": 600,
  "email_token_size": 6,
  "emergency_access_allowed": true,
  "emergency_notification_reminder_schedule": "0 3 * * * *",
  "emergency_request_timeout_schedule": "0 7 * * * *",
  "enable_db_wal": true,
  "event_cleanup_schedule": "0 10 0 * * *",
  "events_days_retain": null,
  "experimental_client_feature_flags": "fido2-vault-credentials",
  "extended_logging": true,
  "helo_name": null,
  "hibp_api_key": null,
  "icon_blacklist_non_global_ips": true,
  "icon_blacklist_regex": null,
  "icon_cache_folder": "data/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_expiration_hours": 120,
  "invitation_org_name": "Vaultwarden",
  "invitations_allowed": true,
  "ip_header": "X-Forwarded-For",
  "job_poll_interval_ms": 30000,
  "log_file": "/data/vaultwarden.log",
  "log_level": "DEBUG",
  "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": "",
  "org_events_enabled": false,
  "org_groups_enabled": false,
  "password_hints_allowed": true,
  "password_iterations": 600000,
  "push_enabled": true,
  "push_identity_uri": "https://identity.bitwarden.eu",
  "push_installation_id": "***",
  "push_installation_key": "***",
  "push_relay_uri": "https://push.bitwarden.eu",
  "reload_templates": false,
  "require_device_email": false,
  "rsa_key_filename": "data/rsa_key",
  "send_purge_schedule": "0 5 * * * *",
  "sendmail_command": null,
  "sends_allowed": true,
  "sends_folder": "data/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_embed_images": true,
  "smtp_explicit_tls": null,
  "smtp_from": "******************",
  "smtp_from_name": "Vaultwarden",
  "smtp_host": "*****************",
  "smtp_password": "***",
  "smtp_port": 587,
  "smtp_security": "starttls",
  "smtp_ssl": null,
  "smtp_timeout": 15,
  "smtp_username": "***************",
  "sso_acceptall_invites": true,
  "sso_authority": "https://myname.ui.com/gw/idp/api/v1/public/oauth/fd97b580-39f9-4735-a780-cc3cd68a9215",
  "sso_callback_path": "https://vault.domain.tld/identity/connect/oidc-signin",
  "sso_client_id": "***",
  "sso_client_secret": "***",
  "sso_enabled": true,
  "sso_key_filepath": "data/sso_key.pub.pem",
  "sso_master_password_policy": null,
  "sso_only": true,
  "sso_organizations_invite": false,
  "sso_organizations_token_path": "/groups",
  "sso_roles_default_to_user": true,
  "sso_roles_enabled": false,
  "sso_roles_token_path": "/resource_access/ipugqevjj7ir5zp2onqqbugrn/roles",
  "sso_scopes": "\"email profile openid offline_access\"",
  "templates_folder": "data/templates",
  "tmp_folder": "data/tmp",
  "trash_auto_delete_days": null,
  "trash_purge_schedule": "0 5 0 * * *",
  "use_sendmail": false,
  "use_syslog": false,
  "user_attachment_limit": null,
  "web_vault_enabled": true,
  "web_vault_folder": "web-vault/",
  "websocket_address": "0.0.0.0",
  "websocket_enabled": false,
  "websocket_port": 3012,
  "yubico_client_id": null,
  "yubico_secret_key": null,
  "yubico_server": null
}
  • vaultwarden version: 1.30.1-5 working , everything greater not
  • Install method: docker compose

  • Clients used: web vault, desktop, Android

  • Reverse proxy and version: Cloudflare

  • Other relevant details:

Steps to reproduce

Use Unitfi Identity Enterprise as SSO provider and use anthing > 1.30.1-5

Expected behaviour

Staying signed in until SSO asks for re-auth like before fixing #16 with 1.30.1-6
Honestly looking at UID Enterprise SSO .well-known/openid-configuration they seem not to support offline_access so I might be in trouble.

Actual behaviour

Getting logged out with "session expired" after 1 hour.
Debug log showing refresh_token is missing

[2024-02-02 18:06:52.869][vaultwarden::sso][ERROR] Impossible to retrieve new access token, refresh_token is missing
[2024-02-02 18:06:52.869][vaultwarden::api::identity][ERROR] {"ErrorModel":{"Message":"Impossible to retrieve new access token, refresh_token is missing","Object":"error"},"ExceptionMessage":null,"ExceptionStackTrace":null,"InnerExceptionMessage":null,"Message":"Impossible to retrieve new access token, refresh_token is missing","Object":"error","ValidationErrors":{"":["Impossible to retrieve new access token, refresh_token is missing"]},"error":"","error_description":""}
[2024-02-02 18:06:52.869][response][INFO] (login) POST /identity/connect/token => 401 Unauthorized

Troubleshooting data

  • Sharing my .well-known/openid-configuration from Unifi Identity Enterprise tenant:
{"issuer":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931","authorization_endpoint":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/authorize","token_endpoint":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/token","jwks_uri":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/keys","userinfo_endpoint":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/userinfo","response_types_supported":["code","code id_token","code id_token token","code token","id_token","id_token token"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"scopes_supported":["email","profile","openid"],"token_endpoint_auth_methods_supported":["client_secret_basic"],"claims_supported":["iss","sub","email","email_verified","name","given_name","family_name"],"end_session_endpoint":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/logout","revocation_endpoint":"https://myname.ui.com/gw/idp/api/v1/public/oauth/97416e24-0e80-4ffa-84fa-930c3d581931/revoke"}
  • Also sharing jwt.io example decode of access_token (w/o sensitive info and not matching above time but you get the point... 1h and no "refresh")
{
  "nbf": 1706809837,     <= ~Feb 1st 18:50:37
  "exp": 1706813437,    <= ~ Feb 1st 19:50:37
  "iss": "https://vault.domain.tld|login",
  "sub": "sso",
  "device_token": "**********************************************************************==",
  "token": {
    "Access": "***********************************************************************"
  }
}

Thank's and keep up the great work!

Hey,

Without refresh_token it will be complicated to extend the session.
Running latest version with LOG_LEVEL=debug I expect you will see a log No refresh_token present and no errors.

In theory offline_access should not be the only way to obtain a refresh_token, cf in the spec:

The use of Refresh Tokens is not exclusive to the offline_access use case. The Authorization Server MAY grant Refresh Tokens in other contexts that are beyond the scope of this specification.

And in a way this scope is a crutch since it's meant to be used when

Refresh Token be issued that can be used to obtain an Access Token that grants access to the End-User's UserInfo Endpoint even when the End-User is not present (not logged in).

But in the VW case the user is present.

Of course all this won't help if your SSO does not implement-it, wanted to mention it because there might be another way to ensure that a refresh_token is included when calling the token endpoint. Had a quick look but couldn't find much documentation :(.

Outside of it only solutions I could see are :

  • extend the session lifetime but it would then apply to all you application and there would be no idle logic.
  • add a config to use the default VaultWarden session handling and ignore the sso session information.
    But it would mean you would loose the ability to invalidate the session since it would stop checking the access_token.
ReqX commented

Thx Jacques for fast reply and insights. Will try to open a ticket with Unifi about the refresh_token ability. Mabye there is a way to generate it.

Of course you were right on the No refresh_token present, an example from my log:

[vaultwarden::sso][DEBUG] No refresh_token present
[vaultwarden::api::identity][INFO] User me@domain.tld logged in successfully. IP: *.*.*.*
[response][INFO] (login) POST /identity/connect/token => 200 OK

On your solution proposals:
For our use case the extension of lifetime would be bad, as you correctly guessed, but will still look into it (maybe a policy for VW only could be done in the Unifi config).

On the other hand, the idea with a config to use normal VW session handling sounds like a great idea on first glance, but only (as in our case) when using SSO as a crutch for onboarding I'd guess? Loosing the ability to invalidate sessions would be acceptable as we'd delete the user from VW anyway if offboarding. Still considering this is nothing that I think would help to promote your SSO solution to be integrated in upstream as yet another layer of complexity (and probably yet another .env var). You probably can't support all edge cases. However, I honestly have not enough know-how on the matter to advocate for such a solution and if Unifi Enterprise (as SSO) is the only one affected or if this might be a broader issue with more common SSO providers.

ReqX commented

The expected response from Unifi

Hi, 

The current version does not support refresh tokens.

We will take this into account for a future release.

Thank you for your feedback!

https://community.ui.com/questions/Unifi-Identiy-Enterprise-as-SSO-provider-no-refreshtoken-offlineaccess-scope/90b15b29-3e79-45b5-8e9e-da83f984fe5c

You probably can't support all edge cases.

Nop ^^, but I expect that it might be useful for people who don't really care about the session life.
I'll check if it can be added cleanly.

Hey
Since it was quite clean added the SSO_AUTH_ONLY_NOT_SESSION setting.
Will be included in 1.30.2-2 which should finish building in ~30min.

ReqX commented

Hey, just pulled ~2h ago and so far it looks perfect!
Logs clean and behavior as expected :-)

Edit: will try to find some time to update SSO.md with UID Enterprise Identity specifics and do a PR.

ReqX commented

Sorry to reopen, but it seems my first response was wrong.

Now tested with mobile (android) client in more details and I seem to have the same behavior as before..
Only difference is that with trace enabled I can now see the refresh_token (with 365 days lifetime when checking with jwt.io), instead of Impossible to retrieve new access token, refresh_token is missing, as before.

Is the ("client_id", "undefined") expected?

[2024-02-06 19:09:07.366][rocket::form::parser::_][TRACE] url-encoded field: ("grant_type", "refresh_token")
[2024-02-06 19:09:07.366][rocket::form::parser::_][TRACE] url-encoded field: ("client_id", "undefined")
[2024-02-06 19:06:08.381][rocket::form::parser::_][TRACE] url-encoded field: ("refresh_token", "*************"l

Here the decoded refresh_token:

{
  "nbf": 1707241143,       #<= Tue Feb 06 2024 18:39:03 GMT +0100
  "exp": 1737999543,       #<= Mon Jan 27 2025 18:39:03 GMT +0100
  "iss": "https://vault.domain.tld|login",
  "sub": "sso",
  "device_token": "************************************==",
  "token": null
}

The log goes on with:

[2024-02-06 19:06:08.381][vaultwarden::auth][ERROR] Token has expired
[2024-02-06 19:06:08.381][vaultwarden::auth][ERROR] Impossible to read refresh_token: {"ErrorModel":{"Message":"Token has expired","Object":"error"},"ExceptionMessage":null,"ExceptionStackTrace":null,"InnerExceptionMessage":null,"Message":"Token has expired","Object":"error","ValidationErrors":{"":["Token has expired"]},"error":"","error_description":""}
[2024-02-06 19:06:08.381][vaultwarden::api::identity][ERROR] {"ErrorModel":{"Message":"Impossible to read refresh_token: {\"ErrorModel\":{\"Message\":\"Token has expired\",\"Object\":\"error\"},\"ExceptionMessage\":null,\"ExceptionStackTrace\":null,\"InnerExceptionMessage\":null,\"Message\":\"Token has expired\",\"Object\":\"error\",\"ValidationErrors\":{\"\":[\"Token has expired\"]},\"error\":\"\",\"error_description\":\"\"}","Object":"error"},"ExceptionMessage":null,"ExceptionStackTrace":null,"InnerExceptionMessage":null,"Message":"Impossible to read refresh_token: {\"ErrorModel\":{\"Message\":\"Token has expired\",\"Object\":\"error\"},\"ExceptionMessage\":null,\"ExceptionStackTrace\":null,\"InnerExceptionMessage\":null,\"Message\":\"Token has expired\",\"Object\":\"error\",\"ValidationErrors\":{\"\":[\"Token has expired\"]},\"error\":\"\",\"error_description\":\"\"}","Object":"error","ValidationErrors":{"":["Impossible to read refresh_token: {\"ErrorModel\":{\"Message\":\"Token has expired\",\"Object\":\"error\"},\"ExceptionMessage\":null,\"ExceptionStackTrace\":null,\"InnerExceptionMessage\":null,\"Message\":\"Token has expired\",\"Object\":\"error\",\"ValidationErrors\":{\"\":[\"Token has expired\"]},\"error\":\"\",\"error_description\":\"\"}"]},"error":"","error_description":""}
[2024-02-06 19:06:08.381][response][INFO] (login) POST /identity/connect/token => 401 Unauthorized

Please do not ask me what I tested before, obviously my celebration was premature :)

Will do some tests again but I'm a bit perplexed on how a token with exp: 1737999543 would be expired.

Note this logic is not specific to the SSO, do you have the same errors when logging with email+master password ?

ReqX commented

Hi, no I do not have that issue, even tested with both SSO_ONLY=false and in parallel with latest upstream.

Honestly, I am somewhat lost as well but before investing any more work into this, please wait for my feedback.
I reopend based on multiple user's feedback and my own tests, but now after checking your code changes and onboarding a brand new user this evening that has NO issues, I realized that all of the testers had active sessions on their mobiles from before the update (with user/pass only).

I will do a clean and proper test tomorrow with a reinstalled/data cleared mobile app, as the same is not happening in Web or Windows client, and that obviously does not make any sense... .


Edit @Timshel ๐Ÿฅ‡
Hi again, we can keep the issue closed and solved!
It was indeed a problem with the old sessions and it's embarrassing I fell for it :)