[Feature] OIDC Refresh Tokens #2190

Closed
opened 2026-02-28 20:05:58 -06:00 by GiteaMirror · 4 comments
Owner

Originally created by @joneshf on GitHub (Jun 11, 2025).

Verified feature request does not already exist?

  • I have searched and found no existing issue

💻

  • Would you like to implement this feature?

Pitch: what problem are you trying to solve?

The best practice for an OpenID Provider (OP) is to use short-lived access tokens (some default to 5-15 minutes). Actual seems to only implement the Authorization Code Flow without Refresh Tokens. What that means in practice for Actual is that using OIDC logs you out frequently (whenever the Access Token expires) and you're forced to re-login frequently.

Although there's nothing technically wrong with the current implementation, I doubt most folks would expect this UX. I think most folks expect that with OIDC, they would appear to be logged in for a longer amount of time by way of Actual refreshing tokens transparently.

Describe your ideal solution to this problem

It seems like Actual should implement Refresh Tokens.

The openid-client package mentions that it supports Refresh Tokens in the README. It also has slightly more detailed API documentation. So it seems like the package can be used to do the heavy lifting.

I imagine that there would need to be a new column added to the sessions table for storing refresh tokens. Something like:

ALTER TABLE sessions
  ADD COLUMN refresh_token TEXT CHECK (
    CASE
      WHEN refresh_token NOT NULL THEN auth = 'openid'
    END
  )
;

The idea being that if a session's auth column is openid, it can optionally have a Refresh Token. It should also prevent a data anomaly like a session with an auth column of password having a Refresh Token (which doesn't seem to be a valid use case for Actual). Since the refresh_token column is nullable, it should allow the current behavior of not using Refresh Tokens if someone doesn't want to use them, an OP doesn't support them, or whatever other reason. Refresh Tokens should be per-session to support uses cases like a person can logging in from multiple devices.

I'm not familiar enough with OIDC to know whether or not the OP would give back an expiration for a Refresh Token. But regardless, it doesn't seem like it would be useful to store it, as the OP is the source of truth on expiration of the Refresh Token. I.e. the Refresh Token may expire for a different reason on the OP's side that isn't time-related.

In order to implement Refresh Tokens, it seems like one of two things needs to happen:

  • Either an additional scope needs to be added to the authorization request.
  • Or, an additional URL parameter needs to be added to the authorization request.

For OPs that support the offline_access scope (like Authelia, Authentik, Keycloak, etc.) it needs to be added to the scopes requested to implement Refresh Tokens. For OPs that have an ad-hoc method of implementing Refresh tokens using a non-standard URL parameter (Dropbox's token_access_type=offline, Google's access_type=offline, maybe others) it needs to be added to the authorization URL to implement Refresh Tokens.

Refresh tokens are an optional scope/URL parameter. The Design Strategy of Actual seems to hint that this shouldn't actually be optional, but required, in order to limit configuration sprawl. But given that Refresh Tokens are implementation-specific (the OP might support scopes, or an ad-hoc URL parameter) it seems like this will need to be two new OIDC environment variables. Maybe something like ACTUAL_OPENID_SCOPES that is all scopes (e.g. ACTUAL_OPENID_SCOPES='openid email profile offline_access') and ACTUAL_OPENID_AUTHORIZATION_URL_ADDITIONAL_PARAMETERS that get appended to the authorization URL (e.g. ACTUAL_OPENID_AUTHORIZATION_URL_ADDITIONAL_PARAMETERS='access_type=offline&prompt=consent). It's not ideal to have structured data be encoded as strings like this, but I'm not sure there are much better options. In any case, I don't currently have a strong opinion one way or the other how this would be implemented.

I checked the box mentioning that I'd be willing to submit a PR, but I'm not sure when I'd be able to do that. I've looked briefly at the code, but don't fully understand all the code paths to know where to make the change. All this to say, I wouldn't wait for me to implement this feature. Some pointers about where to start implementing would be helpful for myself or anyone else that might be interested in contributing.

Teaching and learning

I think this feature, and the OIDC configuration in general, could be more discoverable with documentation about the scopes requested. Right now, OIDC is requesting openid, email, and profile: a025d2b621/packages/sync-server/src/accounts/openid.js (L160)

These scopes are fine, but near as I can tell, they're not documented anywhere. openid is required, so there's no getting away from that. But it doesn't hurt to say that explicitly. email and profile are used to upsert information for the user:

The reasons for them all make sense, but it's not very visible.

Refresh Tokens need the offline_access scope. Unless folks have done their own research to figure out what the offline_access scope means (and why it's normal to request it) it's an oddly named scope to request. The documentation could definitely benefit from at least explaining that offline_access is a common/normal scope that provides the UX of long lasting sessions in a secure manner, and importantly is required for this feature to work properly.

Originally created by @joneshf on GitHub (Jun 11, 2025). ### Verified feature request does not already exist? - [x] I have searched and found no existing issue ### 💻 - [x] Would you like to implement this feature? ### Pitch: what problem are you trying to solve? The best practice for an OpenID Provider (OP) is to use [short-lived access tokens](https://openid.net/specs/openid-connect-core-1_0.html#TokenLifetime) (some default to 5-15 minutes). Actual seems to only implement the [Authorization Code Flow](https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowAuth) without [Refresh Tokens](https://openid.net/specs/openid-connect-core-1_0.html#RefreshTokens). What that means in practice for Actual is that using OIDC logs you out frequently (whenever the Access Token expires) and you're forced to re-login frequently. Although there's nothing technically wrong with the current implementation, I doubt most folks would expect this UX. I think most folks expect that with OIDC, they would appear to be logged in for a longer amount of time by way of Actual refreshing tokens transparently. ### Describe your ideal solution to this problem It seems like Actual should implement Refresh Tokens. The `openid-client` package mentions that it [supports Refresh Tokens](https://github.com/panva/openid-client/tree/v5.7.1/?tab=readme-ov-file#authorization-code-flow) in the README. It also has slightly more detailed [API documentation](https://github.com/panva/openid-client/blob/v5.7.1/docs/README.md#clientrefreshrefreshtoken-extras). So it seems like the package can be used to do the heavy lifting. I imagine that there would need to be a new column added to the `sessions` table for storing refresh tokens. Something like: ```SQL ALTER TABLE sessions ADD COLUMN refresh_token TEXT CHECK ( CASE WHEN refresh_token NOT NULL THEN auth = 'openid' END ) ; ``` The idea being that if a session's `auth` column is `openid`, it can optionally have a Refresh Token. It should also prevent a data anomaly like a session with an `auth` column of `password` having a Refresh Token (which doesn't seem to be a valid use case for Actual). Since the `refresh_token` column is nullable, it should allow the current behavior of not using Refresh Tokens if someone doesn't want to use them, an OP doesn't support them, or whatever other reason. Refresh Tokens should be per-session to support uses cases like a person can logging in from multiple devices. I'm not familiar enough with OIDC to know whether or not the OP would give back an expiration for a Refresh Token. But regardless, it doesn't seem like it would be useful to store it, as the OP is the source of truth on expiration of the Refresh Token. I.e. the Refresh Token may expire for a different reason on the OP's side that isn't time-related. In order to implement Refresh Tokens, it seems like one of two things needs to happen: - Either an additional scope needs to be added to the authorization request. - Or, an additional URL parameter needs to be added to the authorization request. For OPs that support the [`offline_access` scope](https://openid.net/specs/openid-connect-core-1_0.html#OfflineAccess) (like Authelia, Authentik, Keycloak, etc.) it needs to be added to the scopes requested to implement Refresh Tokens. For OPs that have an ad-hoc method of implementing Refresh tokens using a non-standard URL parameter ([Dropbox's `token_access_type=offline`](https://www.dropbox.com/developers/documentation/http/documentation#oauth2-authorize), [Google's `access_type=offline`](https://developers.google.com/identity/openid-connect/openid-connect#refresh-tokens), maybe others) it needs to be added to the authorization URL to implement Refresh Tokens. Refresh tokens are an optional scope/URL parameter. [The Design Strategy of Actual](https://actualbudget.org/docs/contributing/#the-design-strategy-of-actual) seems to hint that this shouldn't actually be optional, but required, in order to limit configuration sprawl. But given that Refresh Tokens are implementation-specific (the OP might support scopes, or an ad-hoc URL parameter) it seems like this will need to be two new [OIDC environment variables](https://actualbudget.org/docs/config/oauth-auth#environment-variables). Maybe something like `ACTUAL_OPENID_SCOPES` that is all scopes (e.g. `ACTUAL_OPENID_SCOPES='openid email profile offline_access'`) and `ACTUAL_OPENID_AUTHORIZATION_URL_ADDITIONAL_PARAMETERS` that get appended to the authorization URL (e.g. `ACTUAL_OPENID_AUTHORIZATION_URL_ADDITIONAL_PARAMETERS='access_type=offline&prompt=consent`). It's not ideal to have structured data be encoded as strings like this, but I'm not sure there are much better options. In any case, I don't currently have a strong opinion one way or the other how this would be implemented. I checked the box mentioning that I'd be willing to submit a PR, but I'm not sure when I'd be able to do that. I've looked briefly at the code, but don't fully understand all the code paths to know where to make the change. All this to say, I wouldn't wait for me to implement this feature. Some pointers about where to start implementing would be helpful for myself or anyone else that might be interested in contributing. ### Teaching and learning I think this feature, and the OIDC configuration in general, could be more discoverable with documentation about the scopes requested. Right now, OIDC is requesting `openid`, `email`, and `profile`: https://github.com/actualbudget/actual/blob/a025d2b62176a04fec9e03ca7c13a87cd200768b/packages/sync-server/src/accounts/openid.js#L160 These scopes are fine, but near as I can tell, they're not documented anywhere. `openid` is required, so there's no getting away from that. But it doesn't hurt to say that explicitly. `email` and `profile` are used to upsert information for the user: - https://github.com/actualbudget/actual/blob/a025d2b62176a04fec9e03ca7c13a87cd200768b/packages/sync-server/src/accounts/openid.js#L227-L232 - https://github.com/actualbudget/actual/blob/a025d2b62176a04fec9e03ca7c13a87cd200768b/packages/sync-server/src/accounts/openid.js#L258-L267 - https://github.com/actualbudget/actual/blob/a025d2b62176a04fec9e03ca7c13a87cd200768b/packages/sync-server/src/accounts/openid.js#L287-L290 The reasons for them all make sense, but it's not very visible. Refresh Tokens need the `offline_access` scope. Unless folks have done their own research to figure out what the `offline_access` scope means (and why it's normal to request it) it's an oddly named scope to request. The documentation could definitely benefit from at least explaining that `offline_access` is a common/normal scope that provides the UX of long lasting sessions in a secure manner, and importantly is required for this feature to work properly.
GiteaMirror added the needs votesfeature labels 2026-02-28 20:05:58 -06:00
Author
Owner

@github-actions[bot] commented on GitHub (Jun 11, 2025):

Thanks for sharing your idea!

This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesn’t mean we don’t accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution).

The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+

Don’t forget to upvote the top comment with 👍!

@github-actions[bot] commented on GitHub (Jun 11, 2025): :sparkles: Thanks for sharing your idea! :sparkles: This repository uses lodash style issue management for enhancements. That means enhancement issues are automatically closed. This doesn’t mean we don’t accept feature requests, though! We will consider implementing ones that receive many upvotes, and we welcome contributions for any feature requests marked as needing votes (just post a comment first so we can help you make a successful contribution). The enhancement backlog can be found here: https://github.com/actualbudget/actual/issues?q=label%3A%22needs+votes%22+sort%3Areactions-%2B1-desc+ Don’t forget to upvote the top comment with 👍! <!-- feature-auto-close-comment -->
Author
Owner

@Mansarde commented on GitHub (Jun 11, 2025):

Now that's a high-quality feature request.
Not just well researched, links to everything, as well as suggestions for how it could be implemented, but also written in a nice broken-up structure so it's easy to consume.
You don't see that every day. Kudos for putting in the effort, ser! 🍻

@Mansarde commented on GitHub (Jun 11, 2025): Now *that's* a high-quality feature request. Not just well researched, links to everything, as well as suggestions for how it could be implemented, but also written in a nice broken-up structure so it's easy to consume. You don't see that every day. Kudos for putting in the effort, ser! 🍻
Author
Owner

@joneshf commented on GitHub (Jun 11, 2025):

written in a nice broken-up structure so it's easy to consume.

Well I can't take credit for this. I used the feature request issue template. It provided the structure. I would've given less detail without it 😅.

@joneshf commented on GitHub (Jun 11, 2025): > written in a nice broken-up structure so it's easy to consume. Well I can't take credit for this. I used the [feature request issue template](https://github.com/actualbudget/actual/blob/eb35b41c6ddc49dfec94a67442d08a0d2f85c70f/.github/ISSUE_TEMPLATE/feature-request.yml). It provided the structure. I would've given less detail without it 😅.
Author
Owner

@Mansarde commented on GitHub (Jun 11, 2025):

Well I can't take credit for this. I used the feature request issue template. It provided the structure. I would've given less detail without it 😅.

It's the effort and quality content you put in though that deserves appreciation.
I don't do well with praise myself, so if it's the same for you, then you have my apologies.
You'll get my kudos regardless though, as I feel that effort like this should be lauded when it happens, to encourage others to strive for the same. 👏

But I'll shut up about it now though. 🤐

@Mansarde commented on GitHub (Jun 11, 2025): > Well I can't take credit for this. I used the [feature request issue template](https://github.com/actualbudget/actual/blob/eb35b41c6ddc49dfec94a67442d08a0d2f85c70f/.github/ISSUE_TEMPLATE/feature-request.yml). It provided the structure. I would've given less detail without it 😅. It's the effort and quality content you put in though that deserves appreciation. I don't do well with praise myself, so if it's the same for you, then you have my apologies. You'll get my kudos regardless though, as I feel that effort like this should be lauded when it happens, to encourage others to strive for the same. 👏 But I'll shut up about it now though. 🤐
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/actual#2190