Should lightweight access tokens always be introspected?

Are lightweight access tokens meant to be always sent to the introspection endpoint, or is it okay to validate them offline?

My understanding is that checking their public key and their iat, exp, iss, and azp claims is fine.

It’s certainly not a rule. You get from locally validating the signature exactly what you would get from a “normal” access token. Validating the JWT access token, lightweight or not, tells you whether the token can be trusted as a token issued by the expected authority and not tampered with.

By only verifying the lightweight token offline, you only get the information if the token itself is technically valid, nothing else. No scope, no userId (sub), nothing.

This is fine in my specific scenario, as I am invoking the UserInfo endpoint to retrieve the user identifier.

If you don’t need information from the token itself and you are just using it to call the userinfo endpoint, there’s no reason to verify it in your client.

That’s good to know, thank you!

It wasn’t apparent from the OIDC Core specification (at least, it wasn’t to me), so I assumed the worst.

OIDC does not specify the access token, only the id token and the userinfo endpoint.
The access token is handled by OAuth2. In OAuth2, there is no token format required, so for a client, an access token is (just) an opaque token and is only meant to be used to call resource servers. The receiver of an access token, the resource server, has to verify/introspect the token (and the data related to it, either from the JWT payload or from the introspection response), as it acts upon the token.
OIDC only requires the access token to be issued with scope openid to be able to call the userinfo endpoint (which is in this case the resource). The userinfo endpoint will reject the request, if the token is not issued in the proper scope.

The fact that also clients make use of the access token, is due to the fact that it is mostly an JWT and can transport actual data in the payload. And due to the fact that it is not explicitly forbidden. Many developers act like if it’s not forbidden, then it’s implicitly allowed
IMHO not the best way.