# OpenID Connect - 403 Forbidden after Expiration of Access Token

**URL:** <https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690>\
**Category:** Uncategorized\
**Tags:** bug, enhancement, solved, server\
**Created:** [August 10, 2026, 11:37am UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690 "2026-08-10T11:37:58Z")\
**Posts on this page:** 7\
**Page:** 1

<div class="post-metadata">

**Author:** ![Martin-S](https://avatars.discourse-cdn.com/v4/letter/m/9de053/32.png) [@Martin-S](https://support.prodi.gy/u/Martin-S)\
**Post date:** [August 10, 2026, 11:37am UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/1 "2026-08-10T11:37:58Z")

</div>

Hello Explosion team,

We are working on a web application for a customer, which integrates Prodigy for 5 users using Prodigy company license and the Prodigy company plugins. Prodigy is installed into a dedicated virtual environment and started/stopped via a subprocess. The main web application and Prodigy shall use OpenID Connect against Keycloak. Both applications get their own subdomain using an Nginx reverse proxy.

Like some other user(s) in the forum ([Bug Report: Call to get\_session\_questions fails with 403 Unauthorized,](https://support.prodi.gy/t/bug-report-call-to-get-session-questions-fails-with-403-unauthorized/7635)) we also encountered the problem of a response `403 Forbidden` for `/get_session_questions` (or `give_answers` on saving using the save button):

```json
{
    "error": "Unauthorized",
    "detail": "No token provided. Not authenticated.\n"
}

```

This error message is caused by no Prodigy authentication cookies present, because they have expired.  
Normally there is a cookie ` __prodigy_cauth_id_token__ ` and a cookie ` __prodigy_cauth_access_token__ `.

It can be temporarily resolved by refreshing the page, but that loses existing annotations that were not transmitted to the backend, yet.

I was able to determine that this recurring problem is strictly coupled to the `Access Token Lifespan` configured in Keycloak. This value defaults to 1 minute and should be kept at a low value.

So, after one minute or more of idling or annotating without a backend request, the problem occurs. If it takes less than a minute it remains fine. The same I could reproduce for an `Access Token Lifespan` of e.g. 15 minutes where 14 minutes are fine, but 15 minutes or more cause the error.

In the implementation of the authentication in the Prodigy company plugins source code there is a method `ProdigyCompanyAuth.get_tokens_from_auth_code`. This returns a `UserTokensResponse` created from only a small subset of the authentication response `payload`.

```python
{'access_token': '<ACCESS_TOKEN>', 'expires_in': 60, 'refresh_expires_in': 1800, 'refresh_token': '<REFRESH_TOKEN>', 'token_type': 'Bearer', 'id_token': '<ID_TOKEN>', 'not-before-policy': 0, 'session_state': '<SESSION_STATE>', 'scope': 'openid email profile'}

```

Of these fields only `id_token`, `access_token`, `expires_in` and `token_type` are used.  
Then the short `expires_in` value for the `access_token` is used for both the access token and the ID token cookies as expiration duration.

However the longer lived refresh token is not used at all and discarded. I am not an authentication expert myself, but I found out that usually a refresh token is used to get a new access token when necessary.

Can you give recommendations on how we can mitigate the issue or improve the OIDC plugin?

---

<div class="post-metadata">

**Author:** ![honnibal](https://sea2.discourse-cdn.com/flex020/user_avatar/support.prodi.gy/honnibal/32/35_2.png) [@honnibal](https://support.prodi.gy/u/honnibal)\
**Post date:** [August 10, 2026, 11:41am UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/2 "2026-08-10T11:41:35Z")

</div>

Hi Martin,

Thanks for the detailed message on this. We'll take a look for you but it sounds reasonable that the token refresh could be improved here.

---

<div class="post-metadata">

**Author:** ![Martin-S](https://avatars.discourse-cdn.com/v4/letter/m/9de053/32.png) [@Martin-S](https://support.prodi.gy/u/Martin-S)\
**Post date:** [August 10, 2026, 11:44am UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/3 "2026-08-10T11:44:51Z")

</div>

## Minimal Working Example

Or rather "not working" maybe

### Keycloak

Hosted on `http://localhost:8081`

- Go to `Manage > Clients` and create a client using `Create client`
- General settings:

```yaml
Client type: OpenID Connect
Client ID: prodigy
...

```

- Capability config:

```yaml
Client authentication: Off
Authorization: Off
Authentication flow:
    Standard flow: Checked
    ...
...

```

- Login settings:

```yaml
Root URL:
Home URL:
Valid redirect URIs:
- http://prodigy.127-0-0-1.sslip.io/*
Valid post logout redirect URIs:
Web origins:

```

- Save using `Save` button

### Nginx

File `prodigy.nginx.conf`

```sh
server {
	listen 80;
	server_name prodigy.127-0-0-1.sslip.io;

	location / {
		proxy_pass http://127.0.0.1:8080;

		proxy_set_header Host $host;
		proxy_set_header X-Forwarded-Host $host;
		proxy_set_header X-Forwarded-Proto $scheme;
		proxy_set_header X-Forwarded-Port $server_port;
		proxy_set_header X-Real-IP $remote_addr;
		proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
	}

	proxy_buffer_size 1M;
	proxy_buffers 4 1M;
	proxy_busy_buffers_size 2M;
}

```

### Prodigy

File `pyproject.toml` using `uv`

```python
[project]
name = "prodigy-oidc-problem-minimal-example"
version = "0.1.0"
description = "Prodigy OIDC Problem Minimal Example"
readme = "README.md"
requires-python = ">=3.14"
dependencies = [
    "prodigy",
    "prodigy-company-plugins",
]

[tool.uv.sources]
prodigy = { path = "prodigy-1.18.5-py3-none-any.whl" }
prodigy-company-plugins = { path = "prodigy_company_plugins-1.0.0a1-py3-none-any.whl" }

```

File `.env`

```sh
PRODIGY_OIDC_AUTH_ENABLED="1"
PRODIGY_OIDC_DISCOVERY_URL="http://localhost:8081/realms/master/.well-known/openid-configuration"
PRODIGY_OIDC_CLIENT_ID="prodigy"
PRODIGY_OIDC_CLIENT_SECRET="secret"
PRODIGY_DEPLOYED_URL="http://prodigy.127-0-0-1.sslip.io"

```

Further file needed: `example.txt` with arbitrary text, maybe a plain text book e.g. [https://www.gutenberg.org/ebooks/1661.txt.utf-8](https://www.gutenberg.org/ebooks/1661.txt.utf-8).

```sh
set -a; source .env; set +a; uv run prodigy ner.manual example-dataset "blank:en" ./example.txt --label person

```

---

<div class="post-metadata">

**Author:** ![Martin-S](https://avatars.discourse-cdn.com/v4/letter/m/9de053/32.png) [@Martin-S](https://support.prodi.gy/u/Martin-S)\
**Post date:** [August 18, 2026, 11:23am UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/4 "2026-08-18T11:23:43Z")

</div>

Hello Matthew,

thank you for the really quick first response.

Can you provide us with an estimation when you will be able to look into the issue and release a patch of the company plugins?

We will need to implement some temporary workaround and justify this for deployment at the customer.  
So, it would help us if we could say "in X months the temporary workaround will not be necessary anymore".

Greetings  
Martin

---

<div class="post-metadata">

**Author:** ![magdaaniol](https://sea2.discourse-cdn.com/flex020/user_avatar/support.prodi.gy/magdaaniol/32/2787_2.png) [@magdaaniol](https://support.prodi.gy/u/magdaaniol)\
**Post date:** [August 18, 2026, 3:30pm UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/5 "2026-08-18T15:30:46Z")

</div>

Hi @Martin-S!

Thank you for the report. The reproduction steps and your analysis of `get_tokens_from_auth_code` were spot on. The refresh token returned by the token endpoint was indeed being discarded, and the cookie lifetimes were tied to the access token's `expires_in`.

We've fixed this in the company plugins: the refresh token is now stored in an httponly cookie scoped to its own (longer) lifetime, and when the id/access token cookies have expired the server obtains fresh tokens via a `refresh_token` grant and re-sets the cookies on the response. Now requests like `/get_session_questions` and `/give_answers` succeed after idle periods instead of returning 403, and no annotations are lost.

Regarding timing: we expect to ship a patch release of `prodigy-company-plugins` by the end of this week, so your temporary workaround should only be needed until then.

Thanks again for the thorough write-up!

---

<div class="post-metadata">

**Author:** ![magdaaniol](https://sea2.discourse-cdn.com/flex020/user_avatar/support.prodi.gy/magdaaniol/32/2787_2.png) [@magdaaniol](https://support.prodi.gy/u/magdaaniol)\
**Post date:** [August 20, 2026, 2:16pm UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/6 "2026-08-20T14:16:58Z")

</div>

Hi @Martin-S,

We've just released `prodigy-company-plugins` `0.5.2` with the fix. Upgrading is all that's needed. No changes to your Keycloak client or Nginx config should be necessary, and you can keep Access Token Lifespan at its low value.

One thing worth knowing: your session is now bounded by the refresh token's lifetime rather than the access token's. In your payload `refresh_expires_in` is `1800`, which comes from Keycloak's SSO Session Idle. So idling longer than that will still send annotators back to login — if 30 minutes is too short for your annotators, that's the value to raise.

Let us know if it doesn't solve your issue, and thank you again for the report.

---

<div class="post-metadata">

**Author:** ![Martin-S](https://avatars.discourse-cdn.com/v4/letter/m/9de053/32.png) [@Martin-S](https://support.prodi.gy/u/Martin-S)\
**Post date:** [August 24, 2026, 12:52pm UTC](https://support.prodi.gy/t/openid-connect-403-forbidden-after-expiration-of-access-token/7690/7 "2026-08-24T12:52:40Z")

</div>

Hello Magda,

thank you very much for the fast fix and patch release!  
I only noticed your replies today, because by default my notification settings did not enable e-mails being sent.

I can confirm that indeed with the released patch `0.5.2` of the company plugins, the behavior when using OpenID Connect is as expected (based on testing with my minimal working example).

Best regards  
Martin
