Neither list contains offline_access. A client requests exactly the scopes it discovers in the resource metadata, and Authentik issues a refresh token only when offline_access is among them. So the client received an access token and nothing to renew it with — once that token aged out, manual re-auth was the only way forward.
Note that Authentik does support the grant (grant_types_supported includes refresh_token); it simply never issues one because nobody asks.
This is the same trap the audience scope already documents one comment further up in the same file:
"Authentik only evaluates a scope mapping when the client requests that scope by name, and the client only learns scope names from this document."
A scope missing from this document is a scope the client will never request, however the IdP is configured.
Change
Adds OIDC_OFFLINE_ACCESS (default false). When enabled, offline_access is appended to the advertised scopes.
Why opt-in rather than always-on
It is only half the fix. The IdP also needs an offline_access scope mapping on the provider, and advertising a scope the IdP doesn't offer risks an invalid_scope rejection that breaks authentication outright. Defaulting it on would be a live-fire change for every other deployment of this server, including ones whose admins never read the release note. A deployment turns it on after configuring its IdP.
Deploying this (both steps required)
Authentik → shared-memory-mcp provider → add the built-in authentik default OAuth Mapping: offline_access to its Scopes. Verify:
Then reconnect one client and confirm it survives past the previous drop-out window.
While you're in the provider, the Access Token validity setting is worth a look — refresh makes expiry survivable, but a very short access-token lifetime still means frequent refresh round-trips.
Tests
4 tests on the metadata endpoint, written before the implementation and confirmed failing against it:
omits offline_access by default (protects existing deployments)
advertises it when opted in
still advertises the audience scope when enabled — regression guard; dropping it would break auth outright
honours a custom OIDC_AUDIENCE_SCOPE name alongside it
typecheck also caught that buildPhaseStub() needed the new field — worth noting since that stub is what keeps next build working without real secrets.
Verification
39/39 tests passing
pnpm -r typecheck clean
pnpm lint clean
pnpm build succeeds
Note
The default stays false in this PR, so merging alone changes nothing — the fix lands when the two deploy steps above are done.
Fixes MCP clients being kicked back to an interactive login on a short cycle with `requires re-authorization (token expired)`.
## Cause
`/.well-known/oauth-protected-resource` advertised only `openid`, `profile`, `email`, and the audience scope. Confirmed against the live deployment:
```
$ curl -s https://memory.dnspegasus.net/.well-known/oauth-protected-resource | jq .scopes_supported
["openid","profile","email","aud-shared-memory"]
$ curl -s https://auth.dnspegasus.net/application/o/shared-memory-mcp/.well-known/openid-configuration | jq .scopes_supported
["profile","aud-shared-memory","openid","email"]
```
Neither list contains `offline_access`. A client requests exactly the scopes it discovers in the resource metadata, and Authentik issues a refresh token only when `offline_access` is among them. So the client received an access token and **nothing to renew it with** — once that token aged out, manual re-auth was the only way forward.
Note that Authentik *does* support the grant (`grant_types_supported` includes `refresh_token`); it simply never issues one because nobody asks.
This is the same trap the audience scope already documents one comment further up in the same file:
> *"Authentik only evaluates a scope mapping when the client requests that scope by name, and the client only learns scope names from this document."*
A scope missing from this document is a scope the client will never request, however the IdP is configured.
## Change
Adds `OIDC_OFFLINE_ACCESS` (default `false`). When enabled, `offline_access` is appended to the advertised scopes.
## Why opt-in rather than always-on
It is only half the fix. The IdP also needs an `offline_access` scope mapping on the provider, and **advertising a scope the IdP doesn't offer risks an `invalid_scope` rejection that breaks authentication outright.** Defaulting it on would be a live-fire change for every other deployment of this server, including ones whose admins never read the release note. A deployment turns it on *after* configuring its IdP.
## Deploying this (both steps required)
1. **Authentik** → `shared-memory-mcp` provider → add the built-in `authentik default OAuth Mapping: offline_access` to its **Scopes**. Verify:
```bash
curl -s https://auth.example.com/application/o/shared-memory-mcp/.well-known/openid-configuration | jq .scopes_supported
```
2. **`.env`** → `OIDC_OFFLINE_ACCESS=true`, redeploy. Verify:
```bash
curl -s https://memory.example.com/.well-known/oauth-protected-resource | jq .scopes_supported
```
Then reconnect one client and confirm it survives past the previous drop-out window.
While you're in the provider, the **Access Token validity** setting is worth a look — refresh makes expiry survivable, but a very short access-token lifetime still means frequent refresh round-trips.
## Tests
4 tests on the metadata endpoint, written before the implementation and confirmed failing against it:
- omits `offline_access` by default (protects existing deployments)
- advertises it when opted in
- **still advertises the audience scope when enabled** — regression guard; dropping it would break auth outright
- honours a custom `OIDC_AUDIENCE_SCOPE` name alongside it
`typecheck` also caught that `buildPhaseStub()` needed the new field — worth noting since that stub is what keeps `next build` working without real secrets.
## Verification
- 39/39 tests passing
- `pnpm -r typecheck` clean
- `pnpm lint` clean
- `pnpm build` succeeds
## Note
The default stays `false` in this PR, so **merging alone changes nothing** — the fix lands when the two deploy steps above are done.
MCP clients were being kicked back to an interactive login on a short
cycle, reporting "requires re-authorization (token expired)".
Cause: /.well-known/oauth-protected-resource advertised only
openid/profile/email plus the audience scope. A client requests exactly
the scopes it finds there, and Authentik issues a refresh token only when
offline_access is among them — so the client received an access token
with nothing to renew it with. Once that token aged out, re-authenticating
by hand was the only path forward.
This is the same trap the audience scope already documents one comment
further up: a scope missing from this document is a scope the client will
never ask for, however the IdP is configured.
Adds OIDC_OFFLINE_ACCESS (default false). Enabling it appends
offline_access to the advertised scopes.
Left opt-in rather than always-on because it is only half the fix — the
IdP also needs an offline_access scope mapping on the provider, and
advertising a scope the IdP doesn't offer risks an invalid_scope
rejection that would break authentication outright. A deployment turns
this on after configuring its IdP; README documents both halves and how
to verify each.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jknapp
merged commit e0d0d3b2b3 into main2026-08-11 22:21:52 +00:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Fixes MCP clients being kicked back to an interactive login on a short cycle with
requires re-authorization (token expired).Cause
/.well-known/oauth-protected-resourceadvertised onlyopenid,profile,email, and the audience scope. Confirmed against the live deployment:Neither list contains
offline_access. A client requests exactly the scopes it discovers in the resource metadata, and Authentik issues a refresh token only whenoffline_accessis among them. So the client received an access token and nothing to renew it with — once that token aged out, manual re-auth was the only way forward.Note that Authentik does support the grant (
grant_types_supportedincludesrefresh_token); it simply never issues one because nobody asks.This is the same trap the audience scope already documents one comment further up in the same file:
A scope missing from this document is a scope the client will never request, however the IdP is configured.
Change
Adds
OIDC_OFFLINE_ACCESS(defaultfalse). When enabled,offline_accessis appended to the advertised scopes.Why opt-in rather than always-on
It is only half the fix. The IdP also needs an
offline_accessscope mapping on the provider, and advertising a scope the IdP doesn't offer risks aninvalid_scoperejection that breaks authentication outright. Defaulting it on would be a live-fire change for every other deployment of this server, including ones whose admins never read the release note. A deployment turns it on after configuring its IdP.Deploying this (both steps required)
shared-memory-mcpprovider → add the built-inauthentik default OAuth Mapping: offline_accessto its Scopes. Verify:.env→OIDC_OFFLINE_ACCESS=true, redeploy. Verify:Then reconnect one client and confirm it survives past the previous drop-out window.
While you're in the provider, the Access Token validity setting is worth a look — refresh makes expiry survivable, but a very short access-token lifetime still means frequent refresh round-trips.
Tests
4 tests on the metadata endpoint, written before the implementation and confirmed failing against it:
offline_accessby default (protects existing deployments)OIDC_AUDIENCE_SCOPEname alongside ittypecheckalso caught thatbuildPhaseStub()needed the new field — worth noting since that stub is what keepsnext buildworking without real secrets.Verification
pnpm -r typecheckcleanpnpm lintcleanpnpm buildsucceedsNote
The default stays
falsein this PR, so merging alone changes nothing — the fix lands when the two deploy steps above are done.