OpenID Connect - 403 Forbidden after Expiration of Access Token

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,) we also encountered the problem of a response 403 Forbidden for /get_session_questions (or give_answers on saving using the save button):

{
    "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.

{'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?

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.

1 Like

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:
Client type: OpenID Connect
Client ID: prodigy
...
  • Capability config:
Client authentication: Off
Authorization: Off
Authentication flow:
    Standard flow: Checked
    ...
...
  • Login settings:
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

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

[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

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.

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