No way to mint an access token for a bot account when OIDC delegated auth is enabled #2044

Closed
opened 2026-07-25 20:22:52 +00:00 by cedhuf · 5 comments

Context

With delegated OIDC authentication configured ([global.oauth.oidc]), Continuwuity behaves as if compatibility_mode = "exclusive", which is the documented and desired behaviour. What I couldn't find, though, is any remaining way to obtain a plain access token for a bot account, I may well have missed a supported path here, so please point me at it if one exists.

Bots and scripts generally authenticate with a long-lived access token. Today, on an OIDC-only server, there is no supported way to create one. To be clear: this is only about minting a session. Presenting an existing token (Authorization: Bearer …) still works fine, tokens issued before enabling OIDC keep working indefinitely. m.login.token is listed above because it is a login flow that also goes through /login, not because bearer auth is affected.

What I verified (Continuwuity 26.6.2, OIDC delegated to Pocket ID)

  • GET and POST /_matrix/client/v3/login404 M_UNRECOGNIZED ("User-interactive authentication is not available on this server"), for both m.login.password and m.login.token.
  • POST /_matrix/client/v1/login/get_token still exists (401 without auth), but is unusable: the login token it mints can only be redeemed through /login, which is gone.
  • !admin users offers create-user, reset-password, enable-login, disable-login, deactivate, … but nothing that returns an access token.
  • !admin query raw provides raw-get / raw-del / iteration, but no raw-put, so a token row cannot be inserted manually either.
  • !admin users enable-login <user> has no effect here: /login is rejected at the route level, before any per-user flag is consulted.

So create-user hands you a password that cannot be used to log in.

Workaround

Temporarily comment out the [global.oauth.oidc] block, restart, POST /login with a chosen device_id, then restore the config and restart again.

Two useful observations from doing this:

  • The resulting token is permanent, no expires_in_ms, no refresh_token, unlike an OAuth-issued one, which suits a long-running bot.
  • Re-enabling OIDC does not invalidate existing sessions, so the token keeps working afterwards.

It works, but it means briefly re-opening password login server-wide, which rather defeats the point of running OIDC-exclusive.

Why appservices don't cover this

Appservice registration does provide a static as_token, but an appservice token has no device, which makes E2EE awkward for bots that behave as ordinary clients (e.g. anything built on mautrix in client mode, needing a stable device_id and its own crypto store).

Proposal

An admin command that mints a token directly, never reopening legacy auth: !admin users create-token <user_id> [--device-id <id>] …and/or a one-step convenience wrapper for the common case: !admin users create-bot <localpart> which creates the account and returns a ready-to-use access token. An alternative would be to make the existing enable-login / disable-login flags meaningful under exclusive mode — i.e. allow legacy login only for accounts explicitly flagged, keeping the endpoint closed for everyone else. The primitive already exists, but it currently has no effect.

If a new command is the way to go, I'd lean toward the admin one, since it never re-enables password authentication, but you know the codebase far better than I do.

Happy to help test whichever direction you prefer.

## Context With delegated OIDC authentication configured (`[global.oauth.oidc]`), Continuwuity behaves as if `compatibility_mode = "exclusive"`, which is the documented and desired behaviour. What I couldn't find, though, is any remaining way to obtain a plain access token for a bot account, I may well have missed a supported path here, so please point me at it if one exists. Bots and scripts generally authenticate with a long-lived access token. Today, on an OIDC-only server, there is no supported way to create one. To be clear: this is only about *minting* a session. Presenting an existing token (`Authorization: Bearer …`) still works fine, tokens issued before enabling OIDC keep working indefinitely. `m.login.token` is listed above because it is a login flow that also goes through `/login`, not because bearer auth is affected. ## What I verified (Continuwuity 26.6.2, OIDC delegated to Pocket ID) - `GET` and `POST /_matrix/client/v3/login` → `404 M_UNRECOGNIZED` ("User-interactive authentication is not available on this server"), for both `m.login.password` and `m.login.token`. - `POST /_matrix/client/v1/login/get_token` still exists (401 without auth), but is unusable: the login token it mints can only be redeemed through `/login`, which is gone. - `!admin users` offers `create-user`, `reset-password`, `enable-login`, `disable-login`, `deactivate`, … but nothing that returns an access token. - `!admin query raw` provides `raw-get` / `raw-del` / iteration, but no `raw-put`, so a token row cannot be inserted manually either. - `!admin users enable-login <user>` has **no effect** here: `/login` is rejected at the route level, before any per-user flag is consulted. So `create-user` hands you a password that cannot be used to log in. ## Workaround Temporarily comment out the `[global.oauth.oidc]` block, restart, `POST /login` with a chosen `device_id`, then restore the config and restart again. Two useful observations from doing this: - The resulting token is permanent, no `expires_in_ms`, no `refresh_token`, unlike an OAuth-issued one, which suits a long-running bot. - Re-enabling OIDC does **not** invalidate existing sessions, so the token keeps working afterwards. It works, but it means briefly re-opening password login server-wide, which rather defeats the point of running OIDC-exclusive. ## Why appservices don't cover this Appservice registration does provide a static `as_token`, but an appservice token has no device, which makes E2EE awkward for bots that behave as ordinary clients (e.g. anything built on mautrix in client mode, needing a stable `device_id` and its own crypto store). ## Proposal An admin command that mints a token directly, never reopening legacy auth: `!admin users create-token <user_id> [--device-id <id>] ` …and/or a one-step convenience wrapper for the common case: `!admin users create-bot <localpart> ` which creates the account and returns a ready-to-use access token. An alternative would be to make the existing `enable-login` / `disable-login` flags meaningful under exclusive mode — i.e. allow legacy login only for accounts explicitly flagged, keeping the endpoint closed for everyone else. The primitive already exists, but it currently has no effect. If a new command is the way to go, I'd lean toward the admin one, since it never re-enables password authentication, but you know the codebase far better than I do. Happy to help test whichever direction you prefer.
ginger self-assigned this 2026-07-25 20:42:10 +00:00
ginger added this to the 26.7.0 milestone 2026-07-25 20:42:25 +00:00
Owner

It's worth mentioning that bots are capable of using the OAuth device flow (which mautrix also supports - Gomuks is an example of a program using this) if they are unable to do standard authorization code flows for OAuth2. If a command is added to issue permanent tokens, it will be a workaround, and will not remain in the server forever. Codebases should be migrating to next-gen auth as soon as possible, because legacy auth is... legacy, and rapidly fading away.

For future reference, please write issues yourself. This could have been "I cannot find a way to issue a permanent access token for an account, please add a command for this", rather than a whole thesis.

It's worth mentioning that bots are capable of using the [OAuth device flow](https://spec.matrix.org/v1.19/client-server-api/#device-authorisation-flow) (which mautrix also supports - Gomuks is an example of a program using this) if they are unable to do standard authorization code flows for OAuth2. If a command is added to issue permanent tokens, it will be a workaround, and will not remain in the server forever. Codebases should be migrating to next-gen auth as soon as possible, because legacy auth is... legacy, and rapidly fading away. For future reference, please write issues yourself. This could have been "I cannot find a way to issue a permanent access token for an account, please add a command for this", rather than a whole thesis.
Author

Thanks, that's exactly the pointer I needed, and the architectural point is fair. I'd much rather do this properly than get a workaround that's on borrowed time.

One thing I can't reconcile, though: on 26.6.2 my /_matrix/client/v1/auth_metadata advertises only

"grant_types_supported": ["authorization_code", "refresh_token"]

with no device_authorization_endpoint. So a bot attempting the device flow has nothing to discover. Is it implemented but unadvertised, planned for a later or am I looking in the wrong place?

For context on why people reach for static tokens today: the client side isn't quite there yet either. matrix-dart-sdk (FluffyChat, Commet) currently can't complete even the authorization-code flow against an OIDC-only server, its checkHomeserver() aborts on the legacy /login check before OIDC discovery runs (fix open at famedly/matrix-dart-sdk#2412). That's not an argument against next-gen auth, just why the transition is bumpy right now.

Thanks, that's exactly the pointer I needed, and the architectural point is fair. I'd much rather do this properly than get a workaround that's on borrowed time. One thing I can't reconcile, though: on 26.6.2 my `/_matrix/client/v1/auth_metadata` advertises only ```json "grant_types_supported": ["authorization_code", "refresh_token"] ``` with no `device_authorization_endpoint`. So a bot attempting the device flow has nothing to discover. Is it implemented but unadvertised, planned for a later or am I looking in the wrong place? For context on why people reach for static tokens today: the client side isn't quite there yet either. matrix-dart-sdk (FluffyChat, Commet) currently can't complete even the authorization-code flow against an OIDC-only server, its `checkHomeserver()` aborts on the legacy `/login` check before OIDC discovery runs (fix open at famedly/matrix-dart-sdk#2412). That's not an argument against next-gen auth, just why the transition is bumpy right now.
Author

Two follow-up thoughts, if I may.

First, the device flow still authenticates a user, so getting a token for @hermes-bot means provisioning a separate identity in my IdP for every bot, each with its own credential, and approving in a private window so as not to authorise as myself. A device flow assumes a human who merely lacks a browser; a bot has no human at all.

Second, on "a command issuing permanent tokens would be a workaround", I think that depends on what it issues. If an admin command minted a proper next-gen session (scoped, refreshable, revocable) rather than a permanent legacy token,
wouldn't that be next-gen auth, just with the grant initiated by server authority instead of user consent? The admin can already create and deactivate accounts; requiring an IdP round-trip to provision a bot on their own server seems like ceremony without added security.

Also worth noting: create-user already creates accounts with no IdP identity at all, but nothing can then authenticate as them.

Two follow-up thoughts, if I may. First, the device flow still authenticates a *user*, so getting a token for `@hermes-bot` means provisioning a separate identity in my IdP for every bot, each with its own credential, and approving in a private window so as not to authorise as myself. A device flow assumes a human who merely lacks a browser; a bot has no human at all. Second, on "a command issuing permanent tokens would be a workaround", I think that depends on what it issues. If an admin command minted a proper next-gen session (scoped, refreshable, revocable) rather than a permanent legacy token, wouldn't that be next-gen auth, just with the grant initiated by server authority instead of user consent? The admin can already create and deactivate accounts; requiring an IdP round-trip to provision a bot on their own server seems like ceremony without added security. Also worth noting: `create-user` already creates accounts with no IdP identity at all, but nothing can then authenticate as them.
Owner

if i wanted to talk to an LLM agent id open chatgpt, please interface directly to describe the issue YOU are having

if i wanted to talk to an LLM agent id open chatgpt, please interface directly to describe the issue YOU are having
Author

English is not my first language, so yes i have it reworked by ai. Not for me, but for the comfort of the people i'm talking to, especially on an issue where i try not to make you lose time. But the problem, the tests and the questions are mine, as far as i understand them. I'm using a bot, and i'm trying to make it connect seamlessly to matrix, and i don't think i'm alone on this, that's all.

Anyway, thanks a lot for your time and your response.
Br,

English is not my first language, so yes i have it reworked by ai. Not for me, but for the comfort of the people i'm talking to, especially on an issue where i try not to make you lose time. But the problem, the tests and the questions are mine, as far as i understand them. I'm using a bot, and i'm trying to make it connect seamlessly to matrix, and i don't think i'm alone on this, that's all. Anyway, thanks a lot for your time and your response. Br,
nex closed this issue 2026-07-27 14:21:39 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
continuwuation/continuwuity#2044
No description provided.